Deployment

This commit is contained in:
Zakhar
2026-09-25 19:44:48 +03:00
parent 596adfdc09
commit 066bfb3c0b
24 changed files with 472 additions and 151 deletions
+16
View File
@@ -0,0 +1,16 @@
.aiassistant
.env
.git/
.gitignore
.idea/
.vscode/
.venv/
venv/
projects-data
CHANGELOG.md
__pycache__
deployment
projects
test_main.http
role_id
wrapped_secret_id
+2
View File
@@ -1,2 +1,4 @@
.env .env
projects projects
role_id
wrapped_secret_id
+2 -1
View File
@@ -6,8 +6,9 @@
<sourceFolder url="file://$MODULE_DIR$/agents/issue_triage/tests" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/agents/issue_triage/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/projects/agents" /> <excludeFolder url="file://$MODULE_DIR$/projects/agents" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.14 (Agents)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.10 (agents) (2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>
+1 -1
View File
@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.14 (Agents)" /> <option name="sdkName" value="Python 3.14 (Agents)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (Agents)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (agents) (2)" project-jdk-type="Python SDK" />
</project> </project>
+9
View File
@@ -0,0 +1,9 @@
### Сборка
```shell
docker build -t triage_agent -f deployment/Dockerfile .
```
### Запуск
```shell
./deployment/generate-wrapped-secret-id.sh && docker compose -f deployment/docker-compose.yml up --build
```
+2 -2
View File
@@ -751,7 +751,7 @@ def analyze_repository(inp: AnalysisInput, run_ctx: RunContext) -> AnalysisResul
return AnalysisResult(success=True, graph=graph, res_all=res_all, leaf_clusters=leaf_clusters) return AnalysisResult(success=True, graph=graph, res_all=res_all, leaf_clusters=leaf_clusters)
def to_networkx(dep_graph: "DependencyGraph", include_external: bool = False) -> nx.DiGraph[DependencyNode]: def to_networkx(dep_graph: "DependencyGraph", include_external: bool = False) -> "nx.DiGraph[DependencyNode]":
""" """
Convert a DependencyGraph into a networkx DiGraph. Convert a DependencyGraph into a networkx DiGraph.
@@ -792,7 +792,7 @@ COLS = [
def analyze( def analyze(
G: nx.DiGraph[DependencyNode], G: "nx.DiGraph[DependencyNode]",
graph: DependencyGraph, # NOTE: currently unused in the body graph: DependencyGraph, # NOTE: currently unused in the body
label: str = "", label: str = "",
) -> AnalyzeResult: ) -> AnalyzeResult:
+3 -1
View File
@@ -13,8 +13,10 @@ import sys
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any from typing import List, Dict, Any
import requests import requests
from playwright.sync_api import sync_playwright
from agents.architect_ts.analyzer import CodeAnalyzer from agents.architect_ts.analyzer import CodeAnalyzer
from agents.coder.CoderAgent import CoderAgent
from agents.solution_architect.PlannerAgent import PlannerAgent
from agents.tester_web.TesterAgent import TesterAgent
# ---------- Конфигурация ---------- # ---------- Конфигурация ----------
CONFIG_FILE = "config.json" CONFIG_FILE = "config.json"
+6
View File
@@ -1,3 +1,9 @@
import os
from typing import List
from agents.codebase_analyst.start import Project
class CoderAgent: class CoderAgent:
"""Агент-исполнитель: выполняет подзадачу, генерирует код и применяет изменения.""" """Агент-исполнитель: выполняет подзадачу, генерирует код и применяет изменения."""
+1 -1
View File
@@ -108,7 +108,7 @@ class AgentState(BaseModel):
} }
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any], state_file: Path) -> AgentState: def from_dict(cls, data: Dict[str, Any], state_file: Path) -> "AgentState":
known = { known = {
"schema_version", "issue_id", "agent_id", "state", "updated_at", "schema_version", "issue_id", "agent_id", "state", "updated_at",
"last_processed_comment_id", "pending_question_comment_id", "last_processed_comment_id", "pending_question_comment_id",
+2 -2
View File
@@ -14,7 +14,7 @@ from agents.issue_triage.triage import render_validation_failure, render_plan_co
from agents.registry import agent, AgentRegistry, Capability from agents.registry import agent, AgentRegistry, Capability
from common.llm_client import LLMClient from common.llm_client import LLMClient
from common.youtrack_mcp_client import IssueNotFound, YouTrackMCPClient from common.youtrack_mcp_client import IssueNotFound, YouTrackMCPClient
from contracts.IssueContext import IssueContext, IssueComment from contracts.IssueContext import IssueContext
logger = logging.getLogger("triage_agent") logger = logging.getLogger("triage_agent")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -23,7 +23,7 @@ logger.setLevel(logging.DEBUG)
class TriageContext: class TriageContext:
issue: IssueContext issue: IssueContext
registry: AgentRegistry registry: AgentRegistry
triage_agent: IssueTriageAgent triage_agent: "IssueTriageAgent"
async def run_triage(ctx: TriageContext) -> TriageMarker: async def run_triage(ctx: TriageContext) -> TriageMarker:
@@ -1,3 +1,9 @@
import json
from typing import List, Dict
from agents.codebase_analyst.start import Project
class PlannerAgent: class PlannerAgent:
"""Агент-планировщик: анализирует проект и создаёт план подзадач.""" """Агент-планировщик: анализирует проект и создаёт план подзадач."""
+5
View File
@@ -1,3 +1,8 @@
import time
from typing import List, Dict, Any
from playwright.sync_api import sync_playwright
from agents.codebase_analyst.start import Project from agents.codebase_analyst.start import Project
+82
View File
@@ -0,0 +1,82 @@
import asyncio
import hashlib
import logging
import os
import time
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
ENV_PATH = Path(os.environ.get("VAULT_ENV_PATH", "/vault-secrets/.env"))
MAX_WAIT_SECONDS = 30
WATCH_INTERVAL_SECONDS = 5
def _file_hash() -> str | None:
if not ENV_PATH.exists():
return None
try:
return hashlib.sha256(ENV_PATH.read_bytes()).hexdigest()
except OSError:
return None
def _parse_env_file(path: Path) -> dict[str, str]:
result: dict[str, str] = {}
with path.open() as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
result[key.strip()] = value.strip().strip('"').strip("'")
return result
def wait_for_env_file(timeout: int = MAX_WAIT_SECONDS) -> None:
"""Блокирующее ожидание появления .env (для entrypoint до старта uvicorn)."""
start = time.time()
while not ENV_PATH.exists():
if time.time() - start > timeout:
raise RuntimeError(
f"Vault Agent did not render {ENV_PATH} within {timeout}s"
)
logger.info("Waiting for %s ...", ENV_PATH)
time.sleep(0.5)
time.sleep(1.5)
def load_env(force: bool = False) -> bool:
if not ENV_PATH.exists():
return False
try:
values = _parse_env_file(ENV_PATH)
except OSError as e:
logger.warning("Failed to read %s: %s", ENV_PATH, e)
return False
for key, value in values.items():
if force or key not in os.environ:
os.environ[key] = value
global _last_hash
_last_hash = _file_hash()
return True
def reload_if_changed() -> bool:
current = _file_hash()
if current is None or current == _last_hash:
return False
logger.info("Detected change in %s, reloading secrets", ENV_PATH)
return load_env(force=True)
async def watch_env_file(interval: int = WATCH_INTERVAL_SECONDS) -> None:
while True:
try:
reload_if_changed()
except Exception:
logger.exception("env watcher failed")
await asyncio.sleep(interval)
+107
View File
@@ -0,0 +1,107 @@
import asyncio
import logging
from typing import Optional
import httpx
from agents.issue_triage.IssueTriageAgent import IssueTriageAgent
from agents.issue_triage.context_builder import YouTrackContextBuilder
from agents.registry import AgentRegistry
from common.llm_client import LLMClient
from common.youtrack_mcp_client import YouTrackMCPClient
logger = logging.getLogger(__name__)
class AgentSingleton:
"""
Синглтон для IssueTriageAgent и его зависимостей.
Потокобезопасен через asyncio.Lock — инициализация не гоняется.
"""
_instance: Optional["AgentSingleton"] = None
_lock = asyncio.Lock()
def __init__(self) -> None:
self._agent: Optional[IssueTriageAgent] = None
self._youtrack_mcp: Optional[YouTrackMCPClient] = None
self._http_for_attachments: Optional[httpx.AsyncClient] = None
self._initialized = False
@classmethod
async def get(cls) -> "AgentSingleton":
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
async def ensure_initialized(self) -> None:
"""Инициализация при первом обращении или после reset()."""
if self._initialized:
return
async with self._lock:
if self._initialized:
return
await self._initialize()
self._initialized = True
async def _initialize(self) -> None:
from main import env, env_optional # локальный импорт, чтобы не плодить циклы
logger.info("Initializing AgentSingleton (or re-initializing)")
self._youtrack_mcp = await YouTrackMCPClient(
env("YOUTRACK_MCP_SERVER"),
env("YOUTRACK_MCP_TOKEN"),
).connect()
self._http_for_attachments = httpx.AsyncClient(
headers={"Authorization": f"Bearer {env('YOUTRACK_MCP_TOKEN')}"},
proxy=env_optional("HTTPS_PROXY"),
timeout=httpx.Timeout(30.0, connect=10.0, read=60.0),
follow_redirects=True,
)
self._agent = IssueTriageAgent(
YouTrackContextBuilder(self._youtrack_mcp, self._http_for_attachments),
LLMClient(
base_url=env("LLM_ADDRESS"),
api_key=env("LLM_API_KEY"),
model=env("LLM_MODEL"),
),
AgentRegistry(),
self._youtrack_mcp,
)
@property
def agent(self) -> IssueTriageAgent:
if self._agent is None:
raise RuntimeError("AgentSingleton not initialized. Call ensure_initialized() first.")
return self._agent
@property
def youtrack_mcp(self) -> YouTrackMCPClient:
if self._youtrack_mcp is None:
raise RuntimeError("AgentSingleton not initialized.")
return self._youtrack_mcp
async def reset(self) -> None:
"""Закрывает старые ресурсы и помечает синглтон как неинициализированный."""
async with self._lock:
logger.info("Resetting AgentSingleton (secrets rotated?)")
if self._http_for_attachments is not None:
await self._http_for_attachments.aclose()
if self._youtrack_mcp is not None:
await self._youtrack_mcp.close()
self._agent = None
self._youtrack_mcp = None
self._http_for_attachments = None
self._initialized = False
@classmethod
async def destroy(cls) -> None:
"""Полное уничтожение синглтона — для shutdown."""
if cls._instance is not None:
await cls._instance.reset()
cls._instance = None
+9 -8
View File
@@ -7,6 +7,15 @@ from pydantic import BaseModel
from contracts.ProjectContext import ProjectContext from contracts.ProjectContext import ProjectContext
class IssueComment(BaseModel):
id: str
author: str
body: str
created_at: int
is_agent: bool = False
marker: dict[str, str] = field(default_factory=dict)
@dataclass @dataclass
class IssueContext: class IssueContext:
issue_id: str issue_id: str
@@ -49,11 +58,3 @@ class IssueContext:
parts.append(f"- @{c.author} ({c.created_at}): {c.body}") parts.append(f"- @{c.author} ({c.created_at}): {c.body}")
return "\n".join(parts) return "\n".join(parts)
class IssueComment(BaseModel):
id: str
author: str
body: str
created_at: int
is_agent: bool = False
marker: dict[str, str] = field(default_factory=dict)
+2 -2
View File
@@ -1,7 +1,7 @@
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, Literal
from contracts.base import BaseContract from contracts.base import BaseContract
@@ -58,7 +58,7 @@ class Task(BaseContract):
lines = [ lines = [
f"# Task: {self.description}", f"# Task: {self.description}",
f"# Before start you must ensure that another tasks have been done: {', '.join(self.depends_on)}" if self.depends_on else "", f"# Before start you must ensure that another tasks have been done: {', '.join(self.depends_on)}" if self.depends_on else "",
f"# Acceptance criteria: \n{"\n - ".join(self.acceptance_criteria)}", f"# Acceptance criteria: \n{"\n - ".join(self.acceptance_criteria)}"
] ]
return "\n".join(lines) return "\n".join(lines)
+18
View File
@@ -0,0 +1,18 @@
FROM python:3.13-slim-trixie AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
### Runtime
FROM gcr.io/distroless/python3-debian13:nonroot
WORKDIR /app
COPY --from=builder /app/deps /app/deps
ENV PYTHONPATH="/app/deps"
ENV PYTHONUNBUFFERED=1
COPY . .
USER nonroot
EXPOSE 8000
CMD ["entrypoint.py"]
+29
View File
@@ -0,0 +1,29 @@
pid_file = "/tmp/pidfile"
vault {
address = "https://10.6.2.1:8200"
tls_skip_verify = true
retry {
num_retries = 5
}
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/vault-auth/role_id"
secret_id_file_path = "/vault-auth/wrapped_secret_id"
secret_id_response_wrapping_path = "auth/approle/role/triage_agent/secret-id"
remove_secret_id_file_after_reading = false
}
}
# sink не нужен, если приложению не требуется сам токен Vault
}
template {
source = "/vault-template/template.ctmpl"
destination = "/vault-secrets/.env"
error_on_missing_key = true
perms = "0644"
}
+48
View File
@@ -0,0 +1,48 @@
volumes:
vault-secrets:
driver_opts:
type: tmpfs
device: tmpfs
o: "size=1m,mode=0777,uid=65532,gid=65532"
projects-data:
services:
vault-agent:
image: hashicorp/vault:1.17
command: agent -config=/vault-agent/agent.hcl
cap_add:
- IPC_LOCK
volumes:
- ./agent.hcl:/vault-agent/agent.hcl:ro
- ./env_template.ctmpl:/vault-template/template.ctmpl:ro
- ./../role_id:/vault-auth/role_id:ro
- ./../wrapped_secret_id:/vault-auth/wrapped_secret_id:ro
- vault-secrets:/vault-secrets
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- agent-net
api:
build:
context: ..
dockerfile: deployment/Dockerfile
ports:
- "8173:8000"
group_add:
- "100"
volumes:
- ./../projects-data:/projects
- vault-secrets:/vault-secrets
depends_on:
vault-agent:
condition: service_started
networks:
- agent-net
networks:
agent-net:
driver: bridge
ipam:
config:
- subnet: 10.6.7.0/24
+10
View File
@@ -0,0 +1,10 @@
{{ with secret "internal_secrets/data/agents" }}
YOUTRACK_MCP_SERVER={{ .Data.data.youtrack_mcp_server }}
YOUTRACK_MCP_TOKEN={{ .Data.data.youtrack_agent_mcp_token }}
YOUTRACK_TRIAGE_AUTHOR={{ .Data.data.youtrack_triage_author }}
LLM_ADDRESS={{ .Data.data.llm_address }}
LLM_API_KEY={{ .Data.data.llm_api_key }}
LLM_MODEL={{ .Data.data.llm_model }}
{{ end }}
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
set -euo pipefail
: '
vault write auth/approle/role/triage_agent \
token_policies="agent_policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=24h \
token_bound_cidrs="10.6.0.0/16"
'
VAULT_ADDR="https://10.6.2.1:8200"
if [ -z "${VAULT_TOKEN:-}" ]; then
read -r -s -p "VAULT_TOKEN: " VAULT_TOKEN
echo
fi
TARGET_DIR=${1:-'.'}
ROLE_NAME="triage_agent"
WRAP_TTL="600s"
CONFIG=$(mktemp)
chmod 600 "$CONFIG"
trap 'rm -f "$CONFIG"' EXIT
{
printf 'header = "X-Vault-Token: %s"\n' "$VAULT_TOKEN"
printf 'header = "X-Vault-Wrap-TTL: %s"\n' "$WRAP_TTL"
} > "$CONFIG"
curl -s -X POST -k -K "$CONFIG" \
"$VAULT_ADDR/v1/auth/approle/role/${ROLE_NAME}/secret-id" \
| jq -r '.wrap_info.token // .errors[0]' | tr -d '\n' > wrapped_secret_id
+17
View File
@@ -0,0 +1,17 @@
import logging
import sys
from common.hot_env import wait_for_env_file, load_env
logging.basicConfig(level=logging.INFO)
if __name__ == "__main__":
try:
wait_for_env_file()
load_env(force=True)
except RuntimeError as e:
print(f"FATAL: {e}", file=sys.stderr)
sys.exit(1)
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")
+44 -33
View File
@@ -2,23 +2,35 @@ import logging
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import httpx from fastapi import FastAPI, HTTPException, Depends
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from agents.coders.BaseCoder import CoderAgent from agents.coders.BaseCoder import CoderAgent
from agents.issue_triage.IssueTriageAgent import IssueTriageAgent from agents.issue_triage.IssueTriageAgent import IssueTriageAgent
from agents.issue_triage.context_builder import YouTrackContextBuilder from agents.issue_triage.context_builder import YouTrackContextBuilder
from agents.registry import AgentRegistry from agents.registry import AgentRegistry
from common.gitea_mcp_client import GiteaMCPClient from common.gitea_mcp_client import GiteaMCPClient
from common.hot_env import load_env, reload_if_changed
from common.llm_client import LLMClient from common.llm_client import LLMClient
from common.singleton import AgentSingleton
from common.youtrack_mcp_client import YouTrackMCPClient, IssueNotFound from common.youtrack_mcp_client import YouTrackMCPClient, IssueNotFound
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
load_dotenv() _last_hash: str | None = None
if not load_env(force=False):
logger.warning("No .env file found at startup, will rely on os.environ")
def env_optional(key: str, default: str | None = None) -> str:
value = os.environ.get(key)
if value is None:
return default
return value
def env(key: str) -> str: def env(key: str) -> str:
reload_if_changed()
value = os.environ.get(key) value = os.environ.get(key)
if not value: if not value:
raise RuntimeError(f"Missing required env var: {key}") raise RuntimeError(f"Missing required env var: {key}")
@@ -26,42 +38,43 @@ def env(key: str) -> str:
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
import asyncio
singleton = await AgentSingleton.get()
await singleton.ensure_initialized()
async def watch_and_reset():
while True:
try:
if reload_if_changed():
await singleton.reset()
await singleton.ensure_initialized()
except Exception:
logger.exception("watch_and_reset failed")
await asyncio.sleep(5)
watcher = asyncio.create_task(watch_and_reset())
app.state.youtrack_mcp = await YouTrackMCPClient( app.state.youtrack_mcp = await YouTrackMCPClient(
str(os.getenv('YOUTRACK_MCP_SERVER')), str(os.getenv('YOUTRACK_MCP_SERVER')),
str(os.getenv('YOUTRACK_MCP_TOKEN')), str(os.getenv('YOUTRACK_MCP_TOKEN')),
).connect() ).connect()
#
#
# res = await mcp.call_tool(
# "add_issue_comment",
# {"issueId": "ARCH-229", "text": "test"}
# )
# logger.info(res)
http_for_attachments = httpx.AsyncClient(
headers={"Authorization": f"Bearer {env('YOUTRACK_MCP_TOKEN')}"},
proxy=env("HTTPS_PROXY"),
timeout=httpx.Timeout(30.0, connect=10.0, read=60.0),
follow_redirects=True,
)
app.state.issue_reader_agent = IssueTriageAgent(
YouTrackContextBuilder(app.state.youtrack_mcp, http_for_attachments),
LLMClient(
base_url=env("LLM_ADDRESS"),
api_key=env("LLM_API_KEY"),
model=env("LLM_MODEL"),
),
AgentRegistry(),
app.state.youtrack_mcp
)
yield yield
# ---- shutdown ---- # ---- shutdown ----
watcher.cancel()
await app.state.youtrack_mcp.close() await app.state.youtrack_mcp.close()
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
async def get_agent() -> IssueTriageAgent:
singleton = await AgentSingleton.get()
await singleton.ensure_initialized()
return singleton.agent
@app.get("/") @app.get("/")
async def root(): async def root():
return {"message": "Hello World"} return {"message": "Hello World"}
@@ -73,8 +86,7 @@ async def say_hello(name: str):
@app.get("/consume_task") @app.get("/consume_task")
async def decomposing_issue(issue: str): async def decomposing_issue(issue: str, agent: IssueTriageAgent = Depends(get_agent)):
agent: IssueTriageAgent = app.state.issue_reader_agent
try: try:
logger.info(f"Received an issue: {issue}") logger.info(f"Received an issue: {issue}")
plan = await agent.plan_issue(issue) plan = await agent.plan_issue(issue)
@@ -89,8 +101,7 @@ async def decomposing_issue(issue: str):
return {"error": str(e)} return {"error": str(e)}
@app.get("/code_issue") @app.get("/code_issue")
async def code_issue(issue: str): async def code_issue(issue: str, issue_agent: IssueTriageAgent = Depends(get_agent)):
issue_agent: IssueTriageAgent = app.state.issue_reader_agent
gitea_mcp_client = GiteaMCPClient() gitea_mcp_client = GiteaMCPClient()
coder_agent: CoderAgent = CoderAgent( coder_agent: CoderAgent = CoderAgent(
str(os.getenv("CODER_ID")), str(os.getenv("CODER_ID")),
+14 -99
View File
@@ -1,103 +1,18 @@
a2wsgi==1.10.10 fastapi==0.141.1
adbc_driver_manager==1.12.0 httpx==0.28.1
adbc_driver_postgresql==1.12.0 mcp==2.2.0
adbc_driver_sqlite==1.12.0 mcp_types==2.2.0
aiohttp==3.14.3 networkx==3.7
AppKit==0.2.8 openai==3.19.2
atheris==3.1.0 pandas==3.0.6
beautifulsoup4==4.15.0 playwright==1.63.0
botocore==1.43.102 pydantic==2.13.5
brotli==1.2.0 python-dotenv==1.2.3
brotlicffi==1.2.0.2 PyYAML==6.0.3
cffi==2.1.1 Requests==2.34.2
chardet==7.6.0 tree_sitter==0.26.0
checks==0.2
ConfigParser==7.2.0
curio==1.6
cycler==0.12.1
Cython==3.3.0
docutils==0.23
email_validator==2.3.0
eval_type_backport==0.4.0
exceptiongroup==1.3.1
fastapi_cli==0.0.32
fastparquet==2026.5.0
filelock==4.0.3
Foundation==0.1.0a0.dev1
fqdn==1.5.1
fsspec==2026.9.0
gunicorn==26.2.0
h2==4.4.1
HTMLParser==0.0.2
httptools==0.8.0
httpx2_jsfetch==1.0
hypothesis==6.168.1
importlib_resources==7.1.0
ipython==9.17.1
ipywidgets==8.1.9
isoduration==20.11.0
itsdangerous==2.2.0
Jinja2==3.1.6
jnius==1.1.0
jsonpath_ng==1.8.0
jsonpointer==3.1.1
keyring==25.7.0
lxml==6.1.3
MarkupSafe==3.0.3
matplotlib==3.11.2
mtrand==0.1
numexpr==2.14.2
odfpy==1.4.1
openpyxl==3.1.5
outcome==1.3.0.post0
Pillow==12.3.0
protobuf==7.36.2
psutil==7.2.2
pydantic_extra_types==2.11.1
pydot==4.0.1
pygraphviz==2.0.2
PyInstaller==6.22.3
pyodide==0.0.2
pyOpenSSL==26.4.0
pyperf==2.10.0
PyQt4==4.11.4
PyQt5==5.15.11
pytest==9.1.1
pytest_run_parallel==0.10.0
python_bcrypt==0.3.2
python_calamine==0.8.2
pyxlsb==1.0.10
qtpy==2.4.3
redis==8.1.0
rfc3339_validator==0.1.4
rfc3986_validator==0.1.1
rfc3987==1.3.8
rfc3987_syntax==1.1.0
s3fs==2026.9.0
scipy==1.18.1
scipy_doctest==2.2.0
setuptools==84.0.0
simplejson==4.1.2
sounddevice==0.5.6
Sphinx==9.1.0
SQLAlchemy==2.1.0
sympy==1.14.0
threadpoolctl==3.7.0
toml==0.10.2
tomlkit==0.15.1
traitlets==5.16.1
tree_sitter_html==0.23.2 tree_sitter_html==0.23.2
tree_sitter_javascript==0.25.0 tree_sitter_javascript==0.25.0
tree_sitter_python==0.25.0 tree_sitter_python==0.25.0
tree_sitter_typescript==0.23.2 tree_sitter_typescript==0.23.2
typer==0.27.2 uvicorn==0.54.0
urllib3_secure_extra==0.1.0
watchfiles==1.3.0
webcolors==25.10.0
winloop==0.6.4
wsproto==1.3.2
xlrd==2.0.2
xlsxwriter==3.2.9
xmlrpclib==1.0.1
zstandard==0.25.0
zttp==0.0.34