chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# yuxi-cli
|
||||
|
||||
Yuxi command line client.
|
||||
|
||||
First-stage scope:
|
||||
|
||||
- remote management through `~/.yuxi/config.toml`
|
||||
- browser login
|
||||
- API Key import through `--api-key`
|
||||
- `whoami`, `status`, and `logout`
|
||||
- server discovery and compatibility check for Yuxi `>=0.7.1`
|
||||
- `yuxi kb upload` for knowledge base file uploads
|
||||
- `yuxi agent eval` for running existing Langfuse dataset experiments with a logged-in remote
|
||||
@@ -0,0 +1,50 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=80", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "yuxi-cli"
|
||||
version = "0.1.2"
|
||||
description = "Yuxi command line client"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "Wenjie Zhang" }]
|
||||
license = "MIT"
|
||||
keywords = ["yuxi", "cli", "rag", "knowledge-base", "agent"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx>=0.27",
|
||||
"langfuse>=4.0.0",
|
||||
"packaging>=24",
|
||||
"questionary>=2.1.0",
|
||||
"rich>=13.7",
|
||||
"typer>=0.16",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/xerrors/Yuxi"
|
||||
Repository = "https://github.com/xerrors/Yuxi"
|
||||
Issues = "https://github.com/xerrors/Yuxi/issues"
|
||||
|
||||
[project.scripts]
|
||||
yuxi = "yuxi_cli.main:app"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"pytest>=8",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Yuxi CLI package."""
|
||||
|
||||
__version__ = "0.1.2"
|
||||
@@ -0,0 +1,3 @@
|
||||
from yuxi_cli.main import app
|
||||
|
||||
app()
|
||||
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from langfuse import Langfuse
|
||||
from rich.console import Console
|
||||
|
||||
from yuxi_cli.client import YuxiClient
|
||||
from yuxi_cli.config import ConfigStore
|
||||
|
||||
|
||||
class AgentEvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentEvalOptions:
|
||||
dataset_name: str
|
||||
agent_slug: str
|
||||
experiment_name: str | None = None
|
||||
max_concurrency: int = 1
|
||||
timeout_seconds: float = 900
|
||||
|
||||
|
||||
def _env(name: str, default: str | None = None) -> str | None:
|
||||
value = os.getenv(name)
|
||||
return value if value else default
|
||||
|
||||
|
||||
def build_langfuse_client() -> Langfuse:
|
||||
kwargs: dict[str, Any] = {
|
||||
"public_key": _env("LANGFUSE_PUBLIC_KEY"),
|
||||
"secret_key": _env("LANGFUSE_SECRET_KEY"),
|
||||
}
|
||||
host = _env("LANGFUSE_BASE_URL")
|
||||
if host:
|
||||
kwargs["host"] = host
|
||||
return Langfuse(**kwargs)
|
||||
|
||||
|
||||
def extract_query(item_input: Any) -> str:
|
||||
if isinstance(item_input, str):
|
||||
return item_input
|
||||
if isinstance(item_input, dict):
|
||||
for key in ("input", "query", "question", "prompt"):
|
||||
value = item_input.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value
|
||||
raise AgentEvalError(f"无法从 Langfuse dataset item input 中提取 query: {item_input!r}")
|
||||
|
||||
|
||||
def run_langfuse_agent_experiment(
|
||||
store: ConfigStore,
|
||||
remote_name: str | None,
|
||||
options: AgentEvalOptions,
|
||||
console: Console,
|
||||
*,
|
||||
langfuse_factory=build_langfuse_client,
|
||||
client_factory=YuxiClient,
|
||||
) -> None:
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
if not remote.api_key:
|
||||
raise AgentEvalError(f"remote 尚未登录: {remote.name}。请先运行 yuxi login。")
|
||||
|
||||
if options.max_concurrency < 1:
|
||||
raise AgentEvalError("--max-concurrency 必须大于等于 1")
|
||||
if options.timeout_seconds <= 0:
|
||||
raise AgentEvalError("--timeout-seconds 必须大于 0")
|
||||
|
||||
experiment_name = options.experiment_name or f"yuxi-agent-eval-{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}"
|
||||
langfuse_client = langfuse_factory()
|
||||
dataset = langfuse_client.get_dataset(options.dataset_name)
|
||||
|
||||
def task(*, item, **_kwargs):
|
||||
return _run_agent_eval_item(
|
||||
remote=remote,
|
||||
agent_slug=options.agent_slug,
|
||||
dataset_name=options.dataset_name,
|
||||
experiment_name=experiment_name,
|
||||
item=item,
|
||||
timeout_seconds=options.timeout_seconds,
|
||||
client_factory=client_factory,
|
||||
)
|
||||
|
||||
result = dataset.run_experiment(
|
||||
name=experiment_name,
|
||||
task=task,
|
||||
max_concurrency=options.max_concurrency,
|
||||
metadata={
|
||||
"source": "agent_evaluation",
|
||||
"agent_slug": options.agent_slug,
|
||||
"dataset_name": options.dataset_name,
|
||||
"remote": remote.name,
|
||||
},
|
||||
)
|
||||
console.print(result.format(include_item_results=True))
|
||||
langfuse_client.flush()
|
||||
processed_count = len(result.item_results)
|
||||
total_count = len(dataset.items)
|
||||
if processed_count != total_count:
|
||||
raise AgentEvalError(f"Langfuse experiment 部分失败: {processed_count}/{total_count} 个 item 成功写入")
|
||||
|
||||
|
||||
def _run_agent_eval_item(
|
||||
*,
|
||||
remote,
|
||||
agent_slug: str,
|
||||
dataset_name: str,
|
||||
experiment_name: str,
|
||||
item: Any,
|
||||
timeout_seconds: float,
|
||||
client_factory,
|
||||
) -> str:
|
||||
query = extract_query(item.input)
|
||||
item_id = str(getattr(item, "id", "") or "")
|
||||
request_id = f"eval-{uuid.uuid4()}"
|
||||
evaluation = {
|
||||
"dataset_name": dataset_name,
|
||||
"dataset_item_id": item_id,
|
||||
"experiment_name": experiment_name,
|
||||
}
|
||||
with client_factory(remote, timeout=timeout_seconds) as client:
|
||||
result = client.run_agent_eval(
|
||||
query=query,
|
||||
agent_slug=agent_slug,
|
||||
evaluation=evaluation,
|
||||
meta={"request_id": request_id},
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
|
||||
if result.get("status") != "completed":
|
||||
raise AgentEvalError(f"Agent eval run failed for dataset item {item_id}: {result}")
|
||||
return str(result.get("output") or "")
|
||||
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from yuxi_cli.config import Remote, build_url
|
||||
|
||||
|
||||
class ClientError(Exception):
|
||||
def __init__(self, message: str, *, error_code: str | None = None, status_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.error_code = error_code
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
@dataclass
|
||||
class CLIAuthSession:
|
||||
device_code: str
|
||||
user_code: str
|
||||
verification_uri: str
|
||||
expires_in: int
|
||||
interval: int
|
||||
|
||||
@property
|
||||
def authorize_path(self) -> str:
|
||||
params = urlencode({"user_code": self.user_code})
|
||||
separator = "&" if "?" in self.verification_uri else "?"
|
||||
return f"{self.verification_uri}{separator}{params}"
|
||||
|
||||
|
||||
class YuxiClient:
|
||||
def __init__(self, remote: Remote, timeout: float = 30.0):
|
||||
self.remote = remote
|
||||
self.client = httpx.Client(timeout=timeout)
|
||||
|
||||
def close(self) -> None:
|
||||
self.client.close()
|
||||
|
||||
def __enter__(self) -> YuxiClient:
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc) -> None:
|
||||
self.close()
|
||||
|
||||
def health(self) -> dict:
|
||||
return self._request("GET", "/system/health", auth=False)
|
||||
|
||||
def discovery(self) -> dict:
|
||||
return self._request("GET", "/system/discovery", auth=False)
|
||||
|
||||
def me(self, api_key: str | None = None) -> dict:
|
||||
return self._request("GET", "/auth/me", api_key=api_key)
|
||||
|
||||
def create_cli_session(self) -> CLIAuthSession:
|
||||
data = self._request("POST", "/auth/cli/sessions", json={}, auth=False)
|
||||
return CLIAuthSession(
|
||||
device_code=data["device_code"],
|
||||
user_code=data["user_code"],
|
||||
verification_uri=data["verification_uri"],
|
||||
expires_in=int(data.get("expires_in") or 600),
|
||||
interval=int(data.get("interval") or 2),
|
||||
)
|
||||
|
||||
def exchange_cli_token(self, device_code: str) -> dict:
|
||||
return self._request("POST", "/auth/cli/sessions/token", json={"device_code": device_code}, auth=False)
|
||||
|
||||
def delete_api_key(self, api_key_id: str) -> dict:
|
||||
return self._request("DELETE", f"/user/apikey/{api_key_id}")
|
||||
|
||||
def get_database(self, kb_id: str) -> dict:
|
||||
return self._request("GET", f"/knowledge/databases/{kb_id}")
|
||||
|
||||
def list_databases(self) -> dict:
|
||||
return self._request("GET", "/knowledge/databases")
|
||||
|
||||
def get_knowledge_base_types(self) -> dict:
|
||||
return self._request("GET", "/knowledge/types")
|
||||
|
||||
def get_supported_file_types(self) -> dict:
|
||||
return self._request("GET", "/knowledge/files/supported-types")
|
||||
|
||||
def knowledge_document_exists(self, kb_id: str, filename: str) -> bool:
|
||||
data = self._request(
|
||||
"GET",
|
||||
f"/knowledge/databases/{kb_id}/documents/exists",
|
||||
params={"filename": filename},
|
||||
)
|
||||
return bool(data.get("exists"))
|
||||
|
||||
def upload_knowledge_file(self, kb_id: str, path: Path, *, timeout_seconds: float = 300) -> dict:
|
||||
with path.open("rb") as fp:
|
||||
return self._request(
|
||||
"POST",
|
||||
"/knowledge/files/upload",
|
||||
params={"kb_id": kb_id},
|
||||
files={"file": (path.name, fp, "application/octet-stream")},
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
|
||||
def add_uploaded_documents(self, kb_id: str, items: list[str], params: dict) -> dict:
|
||||
return self._request(
|
||||
"POST",
|
||||
f"/knowledge/databases/{kb_id}/documents/add",
|
||||
json={"items": items, "params": params},
|
||||
)
|
||||
|
||||
def run_agent_eval(
|
||||
self,
|
||||
*,
|
||||
query: str,
|
||||
agent_slug: str,
|
||||
evaluation: dict,
|
||||
meta: dict | None = None,
|
||||
image_content: str | None = None,
|
||||
model_spec: str | None = None,
|
||||
timeout_seconds: float = 900,
|
||||
) -> dict:
|
||||
payload = {
|
||||
"query": query,
|
||||
"agent_slug": agent_slug,
|
||||
"evaluation": evaluation,
|
||||
"meta": meta or {},
|
||||
"image_content": image_content,
|
||||
"model_spec": model_spec,
|
||||
}
|
||||
return self._request("POST", "/agent-invocation/eval/runs", json=payload, timeout=timeout_seconds)
|
||||
|
||||
def authorize_url(self, session: CLIAuthSession) -> str:
|
||||
return build_url(self.remote.url, session.authorize_path)
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
auth: bool = True,
|
||||
api_key: str | None = None,
|
||||
json: Any | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
data: dict | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> dict:
|
||||
headers = {}
|
||||
token = api_key if api_key is not None else self.remote.api_key
|
||||
if auth and token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
url = f"{self.remote.api_base_url}{path if path.startswith('/') else f'/{path}'}"
|
||||
request_kwargs: dict[str, Any] = {"headers": headers}
|
||||
if params is not None:
|
||||
request_kwargs["params"] = params
|
||||
if files is not None:
|
||||
request_kwargs["files"] = files
|
||||
if data is not None:
|
||||
request_kwargs["data"] = data
|
||||
if json is not None:
|
||||
request_kwargs["json"] = json
|
||||
if timeout is not None:
|
||||
request_kwargs["timeout"] = timeout
|
||||
try:
|
||||
response = self.client.request(method, url, **request_kwargs)
|
||||
except httpx.HTTPError as exc:
|
||||
# 网络层错误(连接失败、超时等)没有 HTTP 状态码,视为可重试的瞬时错误。
|
||||
raise ClientError(f"请求远程失败: {exc}") from exc
|
||||
|
||||
if response.status_code >= 400:
|
||||
error_code, error_message = _parse_http_error(response)
|
||||
raise ClientError(error_message, error_code=error_code, status_code=response.status_code)
|
||||
if not response.content:
|
||||
return {}
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as exc:
|
||||
raise ClientError("远程响应不是 JSON") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ClientError("远程响应格式无效")
|
||||
return data
|
||||
|
||||
|
||||
def _parse_http_error(response: httpx.Response) -> tuple[str | None, str]:
|
||||
"""解析远程错误,返回 (机器可读 error code, 人类可读 message)。"""
|
||||
try:
|
||||
detail = response.json().get("detail")
|
||||
except ValueError:
|
||||
detail = response.text.strip()
|
||||
|
||||
if isinstance(detail, dict):
|
||||
error = detail.get("error")
|
||||
message = detail.get("message")
|
||||
if error and message:
|
||||
return str(error), f"{error}: {message}"
|
||||
if error:
|
||||
return str(error), str(error)
|
||||
if message:
|
||||
return None, str(message)
|
||||
if detail:
|
||||
return None, str(detail)
|
||||
return None, f"HTTP {response.status_code}"
|
||||
@@ -0,0 +1,201 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
from collections.abc import Callable
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from yuxi_cli.client import ClientError, YuxiClient
|
||||
from yuxi_cli.config import ConfigStore, Remote
|
||||
from yuxi_cli.discovery import ServerCompatibilityError, ensure_server_compatible
|
||||
|
||||
PENDING_ERRORS = ("authorization_pending", "slow_down")
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def remote_add(store: ConfigStore, name: str, url: str) -> Remote:
|
||||
config = store.load()
|
||||
remote = config.set_remote(name, url)
|
||||
store.save(config)
|
||||
return remote
|
||||
|
||||
|
||||
def remote_use(store: ConfigStore, name: str) -> Remote:
|
||||
config = store.load()
|
||||
remote = config.use_remote(name)
|
||||
store.save(config)
|
||||
return remote
|
||||
|
||||
|
||||
def remote_list(store: ConfigStore, console: Console) -> None:
|
||||
config = store.load()
|
||||
table = Table(show_header=True, header_style="bold")
|
||||
table.add_column("Current", width=7)
|
||||
table.add_column("Name")
|
||||
table.add_column("URL")
|
||||
table.add_column("Auth")
|
||||
for name, remote in config.remotes.items():
|
||||
table.add_row("*" if name == config.current else "", name, remote.url, "API Key" if remote.has_api_key else "-")
|
||||
console.print(table)
|
||||
|
||||
|
||||
def remote_ping(store: ConfigStore, name: str | None, console: Console, client_factory=YuxiClient) -> None:
|
||||
config = store.load()
|
||||
remote = config.get_remote(name)
|
||||
with client_factory(remote) as client:
|
||||
data = client.health()
|
||||
console.print(f"{remote.name}: {data.get('status', 'ok')} {data.get('version', '')}".rstrip())
|
||||
|
||||
|
||||
def login_with_api_key(
|
||||
store: ConfigStore,
|
||||
remote_name: str | None,
|
||||
api_key: str,
|
||||
console: Console,
|
||||
client_factory=YuxiClient,
|
||||
) -> Remote:
|
||||
if not api_key.startswith("yxkey_"):
|
||||
raise CommandError("API Key 格式无效,应以 yxkey_ 开头")
|
||||
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
with client_factory(remote) as client:
|
||||
_ensure_server_compatible(client, "cli.api_key_auth")
|
||||
client.me(api_key=api_key) # 校验 Key 是否可用
|
||||
|
||||
remote.api_key = api_key
|
||||
remote.api_key_id = ""
|
||||
store.save(config)
|
||||
console.print(f"已保存 {remote.name} 的 API Key。")
|
||||
return remote
|
||||
|
||||
|
||||
def login_with_browser(
|
||||
store: ConfigStore,
|
||||
remote_name: str | None,
|
||||
no_open: bool,
|
||||
console: Console,
|
||||
*,
|
||||
client_factory=YuxiClient,
|
||||
open_browser: Callable[[str], bool] = webbrowser.open,
|
||||
sleep: Callable[[float], None] = time.sleep,
|
||||
monotonic: Callable[[], float] = time.monotonic,
|
||||
) -> Remote:
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
|
||||
with client_factory(remote) as client:
|
||||
_ensure_server_compatible(client, "cli.browser_login")
|
||||
session = client.create_cli_session()
|
||||
authorize_url = client.authorize_url(session)
|
||||
console.print(f"授权码: {session.user_code}")
|
||||
console.print(f"浏览器授权地址: {authorize_url}")
|
||||
if not no_open:
|
||||
open_browser(authorize_url)
|
||||
|
||||
deadline = monotonic() + session.expires_in
|
||||
while monotonic() < deadline:
|
||||
try:
|
||||
data = client.exchange_cli_token(session.device_code)
|
||||
api_key = data.get("secret") or ""
|
||||
api_key_meta = data.get("api_key") or {}
|
||||
if not api_key:
|
||||
raise CommandError("远程未返回 API Key secret")
|
||||
|
||||
remote.api_key = api_key
|
||||
remote.api_key_id = str(api_key_meta.get("id") or "")
|
||||
store.save(config)
|
||||
console.print(f"已完成 {remote.name} 的浏览器登录。")
|
||||
return remote
|
||||
except ClientError as exc:
|
||||
if not _should_keep_polling(exc):
|
||||
raise
|
||||
sleep(max(1, session.interval))
|
||||
|
||||
raise CommandError("浏览器授权超时")
|
||||
|
||||
|
||||
def _should_keep_polling(exc: ClientError) -> bool:
|
||||
"""轮询期间是否应继续重试:等待授权、限流,或瞬时网络/服务端错误。"""
|
||||
if exc.error_code in PENDING_ERRORS or str(exc).startswith(PENDING_ERRORS):
|
||||
return True
|
||||
# status_code 为 None 表示网络层错误;5xx 为服务端瞬时错误,均可重试。
|
||||
return exc.status_code is None or exc.status_code >= 500
|
||||
|
||||
|
||||
def whoami(store: ConfigStore, remote_name: str | None, console: Console, client_factory=YuxiClient) -> None:
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
if not remote.api_key:
|
||||
raise CommandError(f"remote 尚未登录: {remote.name}")
|
||||
with client_factory(remote) as client:
|
||||
user = client.me()
|
||||
console.print(f"{user.get('username')} ({user.get('uid')}) - {user.get('role')}")
|
||||
|
||||
|
||||
def status(store: ConfigStore, remote_name: str | None, console: Console, client_factory=YuxiClient) -> None:
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
with client_factory(remote) as client:
|
||||
health = client.health()
|
||||
auth = "未登录"
|
||||
if remote.api_key:
|
||||
try:
|
||||
user = client.me()
|
||||
auth = f"{user.get('username')} ({user.get('uid')})"
|
||||
except ClientError:
|
||||
auth = "API Key 无效"
|
||||
|
||||
table = Table(show_header=False)
|
||||
table.add_row("Remote", remote.name)
|
||||
table.add_row("URL", remote.url)
|
||||
table.add_row("Health", f"{health.get('status', 'ok')} {health.get('version', '')}".rstrip())
|
||||
table.add_row("Auth", auth)
|
||||
console.print(table)
|
||||
|
||||
|
||||
def logout(
|
||||
store: ConfigStore,
|
||||
remote_name: str | None,
|
||||
local_only: bool,
|
||||
console: Console,
|
||||
client_factory=YuxiClient,
|
||||
) -> Remote:
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
if remote.api_key and remote.api_key_id and not local_only:
|
||||
with client_factory(remote) as client:
|
||||
client.delete_api_key(remote.api_key_id)
|
||||
|
||||
remote.api_key = ""
|
||||
remote.api_key_id = ""
|
||||
store.save(config)
|
||||
console.print(f"已退出 {remote.name}。")
|
||||
return remote
|
||||
|
||||
|
||||
def select_login_mode(console: Console) -> str:
|
||||
console.print("选择登录方式:")
|
||||
console.print("> 1. 浏览器登录(推荐)")
|
||||
console.print(" 2. API Key")
|
||||
if not sys.stdin.isatty():
|
||||
return "browser"
|
||||
value = input("直接回车使用浏览器登录,输入 2 使用 API Key: ").strip()
|
||||
return "api_key" if value == "2" else "browser"
|
||||
|
||||
|
||||
def _ensure_server_compatible(client: YuxiClient, required_capability: str) -> None:
|
||||
try:
|
||||
discovery = client.discovery()
|
||||
except ClientError as exc:
|
||||
raise CommandError(f"无法读取服务端 discovery,请确认远程是 Yuxi 0.7.1 或更高版本: {exc}") from exc
|
||||
try:
|
||||
ensure_server_compatible(discovery, required_capability)
|
||||
except ServerCompatibilityError as exc:
|
||||
raise CommandError(str(exc)) from exc
|
||||
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import tomllib
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
DEFAULT_REMOTE_NAME = "local"
|
||||
DEFAULT_REMOTE_URL = "http://localhost:5173"
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Remote:
|
||||
name: str
|
||||
url: str
|
||||
api_key: str = ""
|
||||
api_key_id: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: dict) -> Remote:
|
||||
return cls(
|
||||
name=name,
|
||||
url=normalize_remote_url(str(data.get("url") or "")),
|
||||
api_key=str(data.get("api_key") or ""),
|
||||
api_key_id=str(data.get("api_key_id") or ""),
|
||||
)
|
||||
|
||||
@property
|
||||
def api_base_url(self) -> str:
|
||||
return f"{self.url.rstrip('/')}/api"
|
||||
|
||||
@property
|
||||
def has_api_key(self) -> bool:
|
||||
return bool(self.api_key)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
current: str = DEFAULT_REMOTE_NAME
|
||||
remotes: dict[str, Remote] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> Config:
|
||||
return cls(
|
||||
current=DEFAULT_REMOTE_NAME,
|
||||
remotes={DEFAULT_REMOTE_NAME: Remote(name=DEFAULT_REMOTE_NAME, url=DEFAULT_REMOTE_URL)},
|
||||
)
|
||||
|
||||
def get_remote(self, name: str | None = None) -> Remote:
|
||||
remote_name = name or self.current
|
||||
remote = self.remotes.get(remote_name)
|
||||
if remote is None:
|
||||
raise ConfigError(f"remote 不存在: {remote_name}")
|
||||
return remote
|
||||
|
||||
def set_remote(self, name: str, url: str) -> Remote:
|
||||
remote = self.remotes.get(name)
|
||||
normalized_url = normalize_remote_url(url)
|
||||
if remote is None:
|
||||
remote = Remote(name=name, url=normalized_url)
|
||||
self.remotes[name] = remote
|
||||
else:
|
||||
if remote.url != normalized_url:
|
||||
remote.api_key = ""
|
||||
remote.api_key_id = ""
|
||||
remote.url = normalized_url
|
||||
if not self.current:
|
||||
self.current = name
|
||||
return remote
|
||||
|
||||
def use_remote(self, name: str) -> Remote:
|
||||
remote = self.get_remote(name)
|
||||
self.current = remote.name
|
||||
return remote
|
||||
|
||||
|
||||
class ConfigStore:
|
||||
def __init__(self, path: Path | None = None):
|
||||
self.path = path or default_config_path()
|
||||
|
||||
def load(self) -> Config:
|
||||
if not self.path.exists():
|
||||
return Config.default()
|
||||
try:
|
||||
data = tomllib.loads(self.path.read_text(encoding="utf-8"))
|
||||
except tomllib.TOMLDecodeError as exc:
|
||||
raise ConfigError(f"配置文件 TOML 格式无效: {self.path}") from exc
|
||||
|
||||
raw_remotes = data.get("remotes") or {}
|
||||
remotes: dict[str, Remote] = {}
|
||||
for name, raw_remote in raw_remotes.items():
|
||||
if isinstance(raw_remote, dict):
|
||||
remotes[name] = Remote.from_dict(name, raw_remote)
|
||||
|
||||
if not remotes:
|
||||
remotes = Config.default().remotes
|
||||
|
||||
current = str(data.get("current") or DEFAULT_REMOTE_NAME)
|
||||
if current not in remotes:
|
||||
current = next(iter(remotes))
|
||||
return Config(current=current, remotes=remotes)
|
||||
|
||||
def save(self, config: Config) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 以 0600 创建文件,避免新文件在 umask 下短暂出现可被他人读取的窗口(凭据明文存放)。
|
||||
def _opener(path, flags):
|
||||
return os.open(path, flags, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
with open(self.path, "w", encoding="utf-8", opener=_opener) as fp:
|
||||
fp.write(_render_toml(config))
|
||||
# 已存在的文件不会被 _opener 重置权限,这里再收敛一次。
|
||||
os.chmod(self.path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
|
||||
def default_config_path() -> Path:
|
||||
return Path.home() / ".yuxi" / "config.toml"
|
||||
|
||||
|
||||
def normalize_remote_url(raw_url: str) -> str:
|
||||
value = raw_url.strip()
|
||||
if not value:
|
||||
raise ConfigError("remote URL 不能为空")
|
||||
if "://" not in value:
|
||||
value = f"http://{value}"
|
||||
|
||||
parts = urlsplit(value)
|
||||
if not parts.scheme or not parts.netloc:
|
||||
raise ConfigError(f"remote URL 无效: {raw_url}")
|
||||
if parts.scheme not in {"http", "https"}:
|
||||
raise ConfigError("remote URL 仅支持 http 或 https")
|
||||
|
||||
path = parts.path.rstrip("/")
|
||||
if path == "/api":
|
||||
path = ""
|
||||
elif path.endswith("/api"):
|
||||
path = path[: -len("/api")]
|
||||
|
||||
normalized = urlunsplit((parts.scheme, parts.netloc, path, "", "")).rstrip("/")
|
||||
return normalized or f"{parts.scheme}://{parts.netloc}"
|
||||
|
||||
|
||||
def build_url(remote_url: str, path: str) -> str:
|
||||
base = normalize_remote_url(remote_url).rstrip("/")
|
||||
suffix = path if path.startswith("/") else f"/{path}"
|
||||
return f"{base}{suffix}"
|
||||
|
||||
|
||||
def _render_toml(config: Config) -> str:
|
||||
lines = [f'current = "{_escape(config.current)}"', ""]
|
||||
for name, remote in config.remotes.items():
|
||||
lines.append(f"[remotes.{_format_key(name)}]")
|
||||
lines.append(f'url = "{_escape(remote.url)}"')
|
||||
lines.append(f'api_key = "{_escape(remote.api_key)}"')
|
||||
lines.append(f'api_key_id = "{_escape(remote.api_key_id)}"')
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _format_key(value: str) -> str:
|
||||
if re.match(r"^[A-Za-z0-9_-]+$", value):
|
||||
return value
|
||||
return f'"{_escape(value)}"'
|
||||
|
||||
|
||||
def _escape(value: str) -> str:
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
MIN_SERVER_VERSION = "0.7.1"
|
||||
|
||||
|
||||
class ServerCompatibilityError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def is_server_version_supported(version: str, minimum: str = MIN_SERVER_VERSION) -> bool:
|
||||
try:
|
||||
parsed = Version(version)
|
||||
required = Version(minimum)
|
||||
except InvalidVersion:
|
||||
return False
|
||||
|
||||
if parsed >= required:
|
||||
return True
|
||||
|
||||
if parsed.is_devrelease:
|
||||
return parsed.release >= required.release
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def ensure_server_compatible(discovery: dict, required_capability: str) -> None:
|
||||
version = str(discovery.get("version") or "")
|
||||
if not is_server_version_supported(version):
|
||||
raise ServerCompatibilityError(f"当前 Yuxi 服务版本 {version or 'unknown'} 低于 CLI 要求 {MIN_SERVER_VERSION}")
|
||||
|
||||
if not _capability_enabled(discovery, required_capability):
|
||||
raise ServerCompatibilityError(f"当前 Yuxi 服务未声明支持 {required_capability}")
|
||||
|
||||
|
||||
def _capability_enabled(discovery: dict, capability: str) -> bool:
|
||||
current = discovery.get("capabilities")
|
||||
for part in capability.split("."):
|
||||
if not isinstance(current, dict):
|
||||
return False
|
||||
current = current.get(part)
|
||||
return current is True
|
||||
@@ -0,0 +1,693 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import questionary
|
||||
import typer
|
||||
from questionary import Choice
|
||||
from rich.console import Console
|
||||
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn, TimeElapsedColumn
|
||||
|
||||
from yuxi_cli.client import ClientError, YuxiClient
|
||||
from yuxi_cli.config import ConfigStore, Remote
|
||||
from yuxi_cli.discovery import ServerCompatibilityError, ensure_server_compatible
|
||||
|
||||
ALREADY_UPLOADED_MESSAGE = "File with the same content already exists in this database"
|
||||
ALREADY_EXISTS_MESSAGE = "File with the same filename already exists in this database"
|
||||
DEFAULT_INCLUDE_EXTENSIONS = {".docx", ".html", ".htm", ".md", ".txt"}
|
||||
DEFAULT_EXCLUDE_EXTENSIONS = {".bmp", ".jpeg", ".jpg", ".pdf", ".png", ".tif", ".tiff"}
|
||||
MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
|
||||
MAX_CONCURRENCY = 300
|
||||
DEFAULT_CONCURRENCY = 10
|
||||
UPLOAD_RETRY_ATTEMPTS = 2
|
||||
PROMPT_STYLE = questionary.Style(
|
||||
[
|
||||
("qmark", "fg:#5f6fff bold"),
|
||||
("question", "bold"),
|
||||
("pointer", "fg:#5f6fff bold"),
|
||||
("highlighted", "fg:#5f6fff bold"),
|
||||
("selected", "fg:#3a7d44"),
|
||||
("instruction", "fg:#6b7280"),
|
||||
("answer", "fg:#3a7d44 bold"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class KbUploadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KbUploadOptions:
|
||||
path: Path
|
||||
kb_id: str | None = None
|
||||
yes: bool = False
|
||||
concurrency: int = DEFAULT_CONCURRENCY
|
||||
include_ext: str | None = None
|
||||
exclude_ext: str | None = None
|
||||
force_upload_file: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocalFile:
|
||||
path: Path
|
||||
relative_path: str
|
||||
extension: str
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkippedFile:
|
||||
path: Path
|
||||
relative_path: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExtensionOption:
|
||||
extension: str
|
||||
count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class UploadResult:
|
||||
local_file: LocalFile
|
||||
file_path: str | None = None
|
||||
content_hash: str | None = None
|
||||
size: int | None = None
|
||||
error: str | None = None
|
||||
already_uploaded: bool = False
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
return bool(self.file_path and self.content_hash)
|
||||
|
||||
|
||||
@dataclass
|
||||
class KbUploadSummary:
|
||||
scanned: int
|
||||
skipped: list[SkippedFile] = field(default_factory=list)
|
||||
selected: list[LocalFile] = field(default_factory=list)
|
||||
uploaded: list[UploadResult] = field(default_factory=list)
|
||||
upload_failed: list[UploadResult] = field(default_factory=list)
|
||||
add_response: dict | None = None
|
||||
|
||||
@property
|
||||
def add_failed_count(self) -> int:
|
||||
if not self.add_response:
|
||||
return 0
|
||||
return int(self.add_response.get("failed") or 0)
|
||||
|
||||
@property
|
||||
def already_uploaded_count(self) -> int:
|
||||
return sum(1 for result in self.upload_failed if result.already_uploaded)
|
||||
|
||||
@property
|
||||
def real_upload_failed_count(self) -> int:
|
||||
return len(self.upload_failed) - self.already_uploaded_count
|
||||
|
||||
|
||||
def run_kb_upload(
|
||||
store: ConfigStore,
|
||||
remote_name: str | None,
|
||||
options: KbUploadOptions,
|
||||
console: Console,
|
||||
*,
|
||||
client_factory=YuxiClient,
|
||||
) -> KbUploadSummary:
|
||||
if options.concurrency < 1 or options.concurrency > MAX_CONCURRENCY:
|
||||
raise KbUploadError(f"--concurrency 必须在 1 到 {MAX_CONCURRENCY} 之间")
|
||||
|
||||
config = store.load()
|
||||
remote = config.get_remote(remote_name)
|
||||
if not remote.api_key:
|
||||
raise KbUploadError(f"remote 尚未登录: {remote.name}")
|
||||
|
||||
with client_factory(remote) as client:
|
||||
_ensure_kb_upload_supported(client)
|
||||
kb_types = _load_kb_types(client)
|
||||
database = _resolve_database(client, options.kb_id, kb_types, console)
|
||||
kb_id = str(database.get("kb_id") or "")
|
||||
if not kb_id:
|
||||
raise KbUploadError("知识库详情缺少 kb_id")
|
||||
supported_extensions = _load_supported_extensions(client)
|
||||
|
||||
scanned_files, initial_skipped = scan_local_files(options.path)
|
||||
selected_extensions = None
|
||||
if not options.yes:
|
||||
selected_extensions = _prompt_select_extensions(
|
||||
scanned_files,
|
||||
supported_extensions=supported_extensions,
|
||||
include_ext=options.include_ext,
|
||||
exclude_ext=options.exclude_ext,
|
||||
)
|
||||
selected, skipped_by_type = select_upload_files(
|
||||
scanned_files,
|
||||
supported_extensions=supported_extensions,
|
||||
include_ext=options.include_ext,
|
||||
exclude_ext=options.exclude_ext,
|
||||
selected_extensions=selected_extensions,
|
||||
)
|
||||
summary = KbUploadSummary(
|
||||
scanned=len(scanned_files) + len(initial_skipped),
|
||||
skipped=[*initial_skipped, *skipped_by_type],
|
||||
selected=selected,
|
||||
)
|
||||
|
||||
if not selected:
|
||||
_print_selection_summary(summary, console)
|
||||
raise KbUploadError("没有可上传的文件")
|
||||
|
||||
_print_selection_summary(summary, console)
|
||||
if not options.yes and not typer.confirm("确认上传?", default=True):
|
||||
raise KbUploadError("已取消")
|
||||
|
||||
uploaded, failed, add_response = upload_files(
|
||||
remote,
|
||||
client_factory,
|
||||
kb_id,
|
||||
selected,
|
||||
concurrency=options.concurrency,
|
||||
console=console,
|
||||
force_upload_file=options.force_upload_file,
|
||||
)
|
||||
summary.uploaded = uploaded
|
||||
summary.upload_failed = failed
|
||||
summary.add_response = add_response
|
||||
|
||||
if not uploaded and not summary.already_uploaded_count:
|
||||
_print_final_summary(summary, console)
|
||||
raise KbUploadError("所有文件上传失败,未添加文档记录")
|
||||
|
||||
_print_final_summary(summary, console)
|
||||
if any(not result.already_uploaded for result in failed) or summary.add_failed_count:
|
||||
raise KbUploadError("部分文件处理失败,请查看摘要")
|
||||
return summary
|
||||
|
||||
|
||||
def scan_local_files(path: Path) -> tuple[list[LocalFile], list[SkippedFile]]:
|
||||
root = path.expanduser()
|
||||
if not root.exists():
|
||||
raise KbUploadError(f"路径不存在: {path}")
|
||||
|
||||
if root.is_file():
|
||||
return _local_file_from_path(root, root.name)
|
||||
|
||||
if not root.is_dir():
|
||||
raise KbUploadError(f"路径不是文件或目录: {path}")
|
||||
|
||||
files: list[LocalFile] = []
|
||||
skipped: list[SkippedFile] = []
|
||||
for current in sorted((p for p in root.rglob("*") if p.is_file()), key=lambda p: p.relative_to(root).as_posix()):
|
||||
relative_path = current.relative_to(root).as_posix()
|
||||
if _has_hidden_part(relative_path):
|
||||
skipped.append(SkippedFile(current, relative_path, "hidden"))
|
||||
continue
|
||||
item_files, item_skipped = _local_file_from_path(current, relative_path)
|
||||
files.extend(item_files)
|
||||
skipped.extend(item_skipped)
|
||||
return files, skipped
|
||||
|
||||
|
||||
def _has_hidden_part(relative_path: str) -> bool:
|
||||
return any(part.startswith(".") for part in Path(relative_path).parts)
|
||||
|
||||
|
||||
def select_upload_files(
|
||||
files: list[LocalFile],
|
||||
*,
|
||||
supported_extensions: set[str],
|
||||
include_ext: str | None,
|
||||
exclude_ext: str | None,
|
||||
selected_extensions: set[str] | None = None,
|
||||
) -> tuple[list[LocalFile], list[SkippedFile]]:
|
||||
if selected_extensions is None:
|
||||
include_extensions = parse_extension_list(include_ext) if include_ext else set(DEFAULT_INCLUDE_EXTENSIONS)
|
||||
exclude_extensions = (
|
||||
parse_extension_list(exclude_ext)
|
||||
if exclude_ext
|
||||
else (set() if include_ext else set(DEFAULT_EXCLUDE_EXTENSIONS))
|
||||
)
|
||||
else:
|
||||
include_extensions = selected_extensions
|
||||
exclude_extensions = set()
|
||||
|
||||
selected: list[LocalFile] = []
|
||||
skipped: list[SkippedFile] = []
|
||||
for item in files:
|
||||
if item.extension not in supported_extensions:
|
||||
skipped.append(SkippedFile(item.path, item.relative_path, "unsupported"))
|
||||
continue
|
||||
if item.extension not in include_extensions:
|
||||
reason = "not-selected" if selected_extensions is not None else "not-included"
|
||||
skipped.append(SkippedFile(item.path, item.relative_path, reason))
|
||||
continue
|
||||
if item.extension in exclude_extensions:
|
||||
skipped.append(SkippedFile(item.path, item.relative_path, "excluded"))
|
||||
continue
|
||||
selected.append(item)
|
||||
return selected, skipped
|
||||
|
||||
|
||||
def parse_extension_list(raw: str | None) -> set[str]:
|
||||
if not raw:
|
||||
return set()
|
||||
extensions = set()
|
||||
for part in raw.split(","):
|
||||
value = part.strip().lower()
|
||||
if not value:
|
||||
continue
|
||||
if not value.startswith("."):
|
||||
value = f".{value}"
|
||||
extensions.add(value)
|
||||
return extensions
|
||||
|
||||
|
||||
def _prompt_select_extensions(
|
||||
files: list[LocalFile],
|
||||
*,
|
||||
supported_extensions: set[str],
|
||||
include_ext: str | None,
|
||||
exclude_ext: str | None,
|
||||
) -> set[str]:
|
||||
options = _extension_options(files, supported_extensions)
|
||||
if not options:
|
||||
return set()
|
||||
if not sys.stdin.isatty():
|
||||
raise KbUploadError("非交互环境请传 --yes 或 --include-ext")
|
||||
|
||||
selected_extensions = _initial_selected_extensions(
|
||||
{option.extension for option in options},
|
||||
include_ext=include_ext,
|
||||
exclude_ext=exclude_ext,
|
||||
)
|
||||
selected = _ask_question(
|
||||
questionary.checkbox(
|
||||
"选择要上传的文件类型",
|
||||
choices=_extension_choices(options, selected_extensions),
|
||||
pointer="›",
|
||||
instruction="↑/↓ 移动 · Space 选择/取消 · Enter 确认",
|
||||
style=PROMPT_STYLE,
|
||||
)
|
||||
)
|
||||
if selected is None:
|
||||
raise KbUploadError("已取消")
|
||||
return set(selected)
|
||||
|
||||
|
||||
def _extension_options(files: list[LocalFile], supported_extensions: set[str]) -> list[ExtensionOption]:
|
||||
counts = Counter(item.extension for item in files if item.extension in supported_extensions)
|
||||
return [ExtensionOption(extension, count) for extension, count in sorted(counts.items())]
|
||||
|
||||
|
||||
def _initial_selected_extensions(
|
||||
available_extensions: set[str], *, include_ext: str | None, exclude_ext: str | None
|
||||
) -> set[str]:
|
||||
selected = parse_extension_list(include_ext) if include_ext else set(DEFAULT_INCLUDE_EXTENSIONS)
|
||||
if exclude_ext:
|
||||
selected -= parse_extension_list(exclude_ext)
|
||||
return selected & available_extensions
|
||||
|
||||
|
||||
def upload_files(
|
||||
remote: Remote,
|
||||
client_factory,
|
||||
kb_id: str,
|
||||
files: list[LocalFile],
|
||||
*,
|
||||
concurrency: int,
|
||||
console: Console,
|
||||
force_upload_file: bool = False,
|
||||
) -> tuple[list[UploadResult], list[UploadResult], dict | None]:
|
||||
uploaded: list[UploadResult] = []
|
||||
failed: list[UploadResult] = []
|
||||
add_response: dict | None = None
|
||||
|
||||
def upload_and_add_one(item: LocalFile) -> tuple[UploadResult, dict | None]:
|
||||
if not force_upload_file and _remote_document_exists(remote, client_factory, kb_id, item):
|
||||
return UploadResult(item, error=ALREADY_EXISTS_MESSAGE, already_uploaded=True), None
|
||||
|
||||
result = _upload_one_with_retry(remote, client_factory, kb_id, item)
|
||||
if not result.success:
|
||||
return result, None
|
||||
with client_factory(remote) as client:
|
||||
response = add_uploaded_documents(client, kb_id, [result])
|
||||
return result, response
|
||||
|
||||
def record_result(result: UploadResult, response: dict | None, completed: int) -> None:
|
||||
nonlocal add_response
|
||||
if result.success:
|
||||
uploaded.append(result)
|
||||
if response:
|
||||
add_response = _merge_add_response(add_response, response)
|
||||
return
|
||||
failed.append(result)
|
||||
if result.already_uploaded:
|
||||
console.print(
|
||||
f"[yellow]-[/yellow] {result.local_file.relative_path} ({completed}/{len(files)}): 已上传过,跳过"
|
||||
)
|
||||
return
|
||||
console.print(f"[red]✗[/red] {result.local_file.relative_path} ({completed}/{len(files)}): {result.error}")
|
||||
|
||||
console.print(f"开始上传并添加: {len(files)} 个文件,并发 {concurrency}")
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=concurrency) as executor:
|
||||
pending: set[Future] = set()
|
||||
file_iter = iter(files)
|
||||
|
||||
def submit_next() -> None:
|
||||
try:
|
||||
item = next(file_iter)
|
||||
except StopIteration:
|
||||
return
|
||||
pending.add(executor.submit(upload_and_add_one, item))
|
||||
|
||||
for _ in range(min(concurrency, len(files))):
|
||||
submit_next()
|
||||
|
||||
def completed_results():
|
||||
while pending:
|
||||
done, still_pending = wait(pending, return_when=FIRST_COMPLETED)
|
||||
pending.clear()
|
||||
pending.update(still_pending)
|
||||
for future in done:
|
||||
yield future.result()
|
||||
submit_next()
|
||||
|
||||
if console.is_terminal:
|
||||
progress = Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
MofNCompleteColumn(),
|
||||
TextColumn("{task.percentage:>3.0f}%"),
|
||||
TimeElapsedColumn(),
|
||||
console=console,
|
||||
)
|
||||
completed = 0
|
||||
with progress:
|
||||
task_id = progress.add_task("处理进度", total=len(files))
|
||||
for result, response in completed_results():
|
||||
completed += 1
|
||||
record_result(result, response, completed)
|
||||
progress.advance(task_id)
|
||||
else:
|
||||
completed = 0
|
||||
progress_step = max(1, len(files) // 10)
|
||||
for result, response in completed_results():
|
||||
completed += 1
|
||||
record_result(result, response, completed)
|
||||
if completed == len(files) or completed % progress_step == 0:
|
||||
console.print(f"处理进度: {completed}/{len(files)}")
|
||||
except KeyboardInterrupt as exc:
|
||||
raise KbUploadError("已取消上传队列,请查看已完成的文档记录") from exc
|
||||
|
||||
uploaded.sort(key=lambda item: item.local_file.relative_path)
|
||||
failed.sort(key=lambda item: item.local_file.relative_path)
|
||||
return uploaded, failed, add_response
|
||||
|
||||
|
||||
def _remote_document_exists(remote: Remote, client_factory, kb_id: str, item: LocalFile) -> bool:
|
||||
try:
|
||||
with client_factory(remote) as client:
|
||||
return client.knowledge_document_exists(kb_id, item.relative_path)
|
||||
except Exception:
|
||||
# 存在性检查只是上传前优化;失败时保留原上传路径的行为。
|
||||
return False
|
||||
|
||||
|
||||
def add_uploaded_documents(client: YuxiClient, kb_id: str, uploaded: list[UploadResult]) -> dict:
|
||||
items = [result.file_path for result in uploaded if result.file_path]
|
||||
params = {
|
||||
"content_type": "file",
|
||||
"content_hashes": {result.file_path: result.content_hash for result in uploaded if result.file_path},
|
||||
"file_sizes": {result.file_path: result.size for result in uploaded if result.file_path},
|
||||
"source_paths": {result.file_path: result.local_file.relative_path for result in uploaded if result.file_path},
|
||||
}
|
||||
return client.add_uploaded_documents(kb_id, items, params)
|
||||
|
||||
|
||||
def _merge_add_response(current: dict | None, response: dict) -> dict:
|
||||
if current is None:
|
||||
current = {"items": [], "failed_items": [], "added": 0, "failed": 0}
|
||||
|
||||
current["items"].extend(response.get("items") or [])
|
||||
current["failed_items"].extend(response.get("failed_items") or [])
|
||||
current["added"] += int(response.get("added") or 0)
|
||||
current["failed"] += int(response.get("failed") or 0)
|
||||
|
||||
added = int(current["added"])
|
||||
failed = int(current["failed"])
|
||||
if failed == 0:
|
||||
current["status"] = "success"
|
||||
current["message"] = f"已添加 {added} 个文件"
|
||||
elif added == 0:
|
||||
current["status"] = "failed"
|
||||
current["message"] = f"文件添加失败,失败 {failed} 个"
|
||||
else:
|
||||
current["status"] = "partial_failed"
|
||||
current["message"] = f"已添加 {added} 个文件,失败 {failed} 个"
|
||||
return current
|
||||
|
||||
|
||||
def _local_file_from_path(path: Path, relative_path: str) -> tuple[list[LocalFile], list[SkippedFile]]:
|
||||
if path.is_symlink():
|
||||
return [], [SkippedFile(path, relative_path, "symlink")]
|
||||
try:
|
||||
stat = path.stat()
|
||||
except OSError as exc:
|
||||
return [], [SkippedFile(path, relative_path, f"stat-failed: {exc}")]
|
||||
if stat.st_size == 0:
|
||||
return [], [SkippedFile(path, relative_path, "empty")]
|
||||
if stat.st_size > MAX_UPLOAD_SIZE_BYTES:
|
||||
return [], [SkippedFile(path, relative_path, "too-large")]
|
||||
if not os.access(path, os.R_OK):
|
||||
return [], [SkippedFile(path, relative_path, "unreadable")]
|
||||
extension = path.suffix.lower()
|
||||
if not extension:
|
||||
return [], [SkippedFile(path, relative_path, "no-extension")]
|
||||
local_file = LocalFile(
|
||||
path=path,
|
||||
relative_path=relative_path.replace("\\", "/"),
|
||||
extension=extension,
|
||||
size=stat.st_size,
|
||||
)
|
||||
return [local_file], []
|
||||
|
||||
|
||||
def _ensure_kb_upload_supported(client: YuxiClient) -> None:
|
||||
try:
|
||||
ensure_server_compatible(client.discovery(), "cli.kb_upload")
|
||||
except ServerCompatibilityError as exc:
|
||||
raise KbUploadError(str(exc)) from exc
|
||||
|
||||
|
||||
def _load_kb_types(client: YuxiClient) -> dict:
|
||||
payload = client.get_knowledge_base_types()
|
||||
kb_types = payload.get("kb_types")
|
||||
if not isinstance(kb_types, dict):
|
||||
raise KbUploadError("服务端知识库类型响应格式无效")
|
||||
return kb_types
|
||||
|
||||
|
||||
def _resolve_database(client: YuxiClient, kb_id: str | None, kb_types: dict, console: Console) -> dict:
|
||||
if kb_id and kb_id.strip():
|
||||
database = client.get_database(kb_id.strip())
|
||||
_ensure_database_supports_documents(database, kb_types)
|
||||
return database
|
||||
|
||||
databases = _list_uploadable_databases(client, kb_types)
|
||||
if not databases:
|
||||
raise KbUploadError("当前 remote 没有可用于文档上传的知识库")
|
||||
if len(databases) == 1:
|
||||
database = databases[0]
|
||||
console.print(f"已选择唯一可用知识库: {database.get('name') or database.get('kb_id')}")
|
||||
return database
|
||||
if not sys.stdin.isatty():
|
||||
raise KbUploadError("非交互环境必须显式传入 --kb-id")
|
||||
|
||||
return _prompt_select_database(databases)
|
||||
|
||||
|
||||
def _prompt_select_database(databases: list[dict]) -> dict:
|
||||
selected_index = _ask_question(
|
||||
questionary.select(
|
||||
"选择知识库",
|
||||
choices=_database_choices(databases),
|
||||
pointer="›",
|
||||
instruction="↑/↓ 移动 · Enter 确认",
|
||||
style=PROMPT_STYLE,
|
||||
)
|
||||
)
|
||||
if selected_index is None:
|
||||
raise KbUploadError("已取消")
|
||||
return databases[int(selected_index)]
|
||||
|
||||
|
||||
def _ask_question(question) -> Any:
|
||||
try:
|
||||
return question.ask()
|
||||
except (EOFError, KeyboardInterrupt) as exc:
|
||||
raise KbUploadError("已取消") from exc
|
||||
|
||||
|
||||
def _database_choices(databases: list[dict]) -> list[Choice]:
|
||||
return [Choice(title=_database_option_label(database), value=index) for index, database in enumerate(databases)]
|
||||
|
||||
|
||||
def _database_option_label(database: dict) -> str:
|
||||
name = str(database.get("name") or database.get("database_name") or "-")
|
||||
kb_id = str(database.get("kb_id") or "-")
|
||||
kb_type = str(database.get("kb_type") or "-")
|
||||
return f"{name} [{kb_type}] {kb_id}"
|
||||
|
||||
|
||||
def _extension_choices(options: list[ExtensionOption], selected_extensions: set[str]) -> list[Choice]:
|
||||
return [
|
||||
Choice(
|
||||
title=_extension_option_label(option),
|
||||
value=option.extension,
|
||||
checked=option.extension in selected_extensions,
|
||||
)
|
||||
for option in options
|
||||
]
|
||||
|
||||
|
||||
def _extension_option_label(option: ExtensionOption) -> str:
|
||||
return f"{option.extension.lstrip('.')} ({option.count})"
|
||||
|
||||
|
||||
def _format_unsupported_summary(unsupported_counts: Counter[str]) -> str:
|
||||
total = sum(unsupported_counts.values())
|
||||
extensions = [extension for extension, _count in unsupported_counts.most_common()]
|
||||
visible = extensions[:8]
|
||||
remaining = len(extensions) - len(visible)
|
||||
suffix = f", 等 {remaining} 类" if remaining else ""
|
||||
return f"不支持: {total} ({', '.join(visible)}{suffix})"
|
||||
|
||||
|
||||
def _list_uploadable_databases(client: YuxiClient, kb_types: dict) -> list[dict]:
|
||||
payload = client.list_databases()
|
||||
databases = payload.get("databases")
|
||||
if not isinstance(databases, list):
|
||||
raise KbUploadError("服务端知识库列表响应格式无效")
|
||||
uploadable = []
|
||||
for database in databases:
|
||||
if not isinstance(database, dict):
|
||||
continue
|
||||
if _database_supports_documents(database, kb_types):
|
||||
uploadable.append(database)
|
||||
uploadable.sort(key=lambda item: (str(item.get("name") or ""), str(item.get("kb_id") or "")))
|
||||
return uploadable
|
||||
|
||||
|
||||
def _ensure_database_supports_documents(database: dict, kb_types: dict) -> None:
|
||||
kb_type = str(database.get("kb_type") or "")
|
||||
if not kb_type:
|
||||
raise KbUploadError("知识库详情缺少 kb_type,无法确认是否支持文档上传")
|
||||
|
||||
type_info = kb_types.get(kb_type)
|
||||
if not isinstance(type_info, dict):
|
||||
raise KbUploadError(f"服务端未返回知识库类型信息: {kb_type}")
|
||||
if type_info.get("supports_documents") is False:
|
||||
raise KbUploadError(f"{database.get('name') or kb_type} 只支持检索,不支持文档上传")
|
||||
|
||||
|
||||
def _database_supports_documents(database: dict, kb_types: dict) -> bool:
|
||||
kb_type = str(database.get("kb_type") or "")
|
||||
type_info = kb_types.get(kb_type)
|
||||
return isinstance(type_info, dict) and type_info.get("supports_documents") is True
|
||||
|
||||
|
||||
def _load_supported_extensions(client: YuxiClient) -> set[str]:
|
||||
payload = client.get_supported_file_types()
|
||||
raw_file_types = payload.get("file_types")
|
||||
if not isinstance(raw_file_types, list) or not raw_file_types:
|
||||
raise KbUploadError("服务端支持文件类型响应格式无效")
|
||||
return {str(item).lower() if str(item).startswith(".") else f".{str(item).lower()}" for item in raw_file_types}
|
||||
|
||||
|
||||
def _upload_one_with_retry(remote: Remote, client_factory, kb_id: str, item: LocalFile) -> UploadResult:
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(UPLOAD_RETRY_ATTEMPTS + 1):
|
||||
try:
|
||||
with client_factory(remote) as client:
|
||||
data = client.upload_knowledge_file(kb_id, item.path)
|
||||
file_path = str(data.get("file_path") or data.get("minio_path") or "")
|
||||
content_hash = str(data.get("content_hash") or "")
|
||||
size = int(data.get("size") or item.size)
|
||||
if not file_path or not content_hash:
|
||||
return UploadResult(item, error="上传响应缺少 file_path 或 content_hash")
|
||||
return UploadResult(item, file_path=file_path, content_hash=content_hash, size=size)
|
||||
except ClientError as exc:
|
||||
last_error = exc
|
||||
if _is_already_uploaded(exc):
|
||||
return UploadResult(item, error=ALREADY_UPLOADED_MESSAGE, already_uploaded=True)
|
||||
if not _is_retryable(exc) or attempt >= UPLOAD_RETRY_ATTEMPTS:
|
||||
break
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_error = exc
|
||||
break
|
||||
time.sleep(2**attempt)
|
||||
return UploadResult(item, error=str(last_error) if last_error else "上传失败")
|
||||
|
||||
|
||||
def _is_retryable(exc: ClientError) -> bool:
|
||||
return exc.status_code is None or exc.status_code == 429 or exc.status_code >= 500
|
||||
|
||||
|
||||
def _is_already_uploaded(exc: ClientError) -> bool:
|
||||
return ALREADY_UPLOADED_MESSAGE in str(exc)
|
||||
|
||||
|
||||
def _print_selection_summary(summary: KbUploadSummary, console: Console) -> None:
|
||||
selected_extensions = sorted({item.extension for item in summary.selected})
|
||||
selected_extension_text = f" ({', '.join(selected_extensions)})" if selected_extensions else ""
|
||||
not_selected = sum(1 for item in summary.skipped if item.reason in {"not-selected", "not-included", "excluded"})
|
||||
unsupported_counts = _unsupported_counts_from_skipped(summary.skipped)
|
||||
unsupported_total = sum(unsupported_counts.values())
|
||||
other_skipped = len(summary.skipped) - not_selected - unsupported_total
|
||||
|
||||
console.print("上传预览")
|
||||
console.print(f" 扫描文件: {summary.scanned}")
|
||||
console.print(f" 将上传: {len(summary.selected)}{selected_extension_text}")
|
||||
if not_selected:
|
||||
console.print(f" 未选择: {not_selected}")
|
||||
if unsupported_counts:
|
||||
console.print(f" {_format_unsupported_summary(unsupported_counts)}", markup=False)
|
||||
if other_skipped:
|
||||
console.print(f" 其他跳过: {other_skipped}")
|
||||
|
||||
|
||||
def _unsupported_counts_from_skipped(skipped: list[SkippedFile]) -> Counter[str]:
|
||||
counts = Counter()
|
||||
for item in skipped:
|
||||
if item.reason == "unsupported":
|
||||
counts[item.path.suffix.lower() or "无扩展名"] += 1
|
||||
elif item.reason == "no-extension":
|
||||
counts["无扩展名"] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def _print_final_summary(summary: KbUploadSummary, console: Console) -> None:
|
||||
added = int((summary.add_response or {}).get("added") or 0)
|
||||
add_failed = int((summary.add_response or {}).get("failed") or 0)
|
||||
|
||||
console.print("上传结果")
|
||||
console.print(f" 上传成功: {len(summary.uploaded)}")
|
||||
if summary.already_uploaded_count:
|
||||
console.print(f" 已上传过: {summary.already_uploaded_count}")
|
||||
console.print(f" 上传失败: {summary.real_upload_failed_count}")
|
||||
console.print(f" 添加成功: {added}")
|
||||
console.print(f" 添加失败: {add_failed}")
|
||||
|
||||
failed_items = (summary.add_response or {}).get("failed_items") or []
|
||||
if failed_items:
|
||||
for item in failed_items:
|
||||
console.print(f"[red]添加失败:[/red] {item.get('item')} - {item.get('error')}")
|
||||
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from yuxi_cli import __version__
|
||||
from yuxi_cli.agent_eval import AgentEvalError, AgentEvalOptions, run_langfuse_agent_experiment
|
||||
from yuxi_cli.client import ClientError
|
||||
from yuxi_cli.commands import (
|
||||
CommandError,
|
||||
login_with_api_key,
|
||||
login_with_browser,
|
||||
logout as logout_command,
|
||||
remote_add,
|
||||
remote_list,
|
||||
remote_ping,
|
||||
remote_use,
|
||||
select_login_mode,
|
||||
status as status_command,
|
||||
whoami as whoami_command,
|
||||
)
|
||||
from yuxi_cli.config import ConfigError, ConfigStore
|
||||
from yuxi_cli.kb_upload import DEFAULT_CONCURRENCY, MAX_CONCURRENCY, KbUploadError, KbUploadOptions, run_kb_upload
|
||||
|
||||
console = Console()
|
||||
app = typer.Typer(help="Yuxi command line client.", invoke_without_command=True)
|
||||
remote_app = typer.Typer(help="Manage Yuxi remotes.")
|
||||
agent_app = typer.Typer(help="Run and manage Yuxi agents.")
|
||||
kb_app = typer.Typer(help="Upload and manage knowledge base files.")
|
||||
app.add_typer(remote_app, name="remote")
|
||||
app.add_typer(agent_app, name="agent")
|
||||
app.add_typer(kb_app, name="kb")
|
||||
|
||||
|
||||
def _store() -> ConfigStore:
|
||||
return ConfigStore()
|
||||
|
||||
|
||||
def _print_remote_context(store: ConfigStore, remote_name: str | None) -> None:
|
||||
remote = store.load().get_remote(remote_name)
|
||||
console.print(f"Yuxi CLI {__version__}")
|
||||
console.print(f"Remote: {remote.name} {remote.url}")
|
||||
|
||||
|
||||
def _handle_error(exc: Exception) -> None:
|
||||
console.print(f"[red]错误:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
version: bool = typer.Option(False, "--version", help="Show version and exit.", is_eager=True),
|
||||
):
|
||||
if version:
|
||||
console.print(__version__)
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@remote_app.command("add")
|
||||
def add_remote(name: str, url: str):
|
||||
try:
|
||||
remote = remote_add(_store(), name, url)
|
||||
console.print(f"已保存 remote {remote.name}: {remote.url}")
|
||||
except ConfigError as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@remote_app.command("use")
|
||||
def use_remote(name: str):
|
||||
try:
|
||||
remote = remote_use(_store(), name)
|
||||
console.print(f"当前 remote: {remote.name}")
|
||||
except ConfigError as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@remote_app.command("list")
|
||||
def list_remotes():
|
||||
try:
|
||||
remote_list(_store(), console)
|
||||
except ConfigError as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@remote_app.command("ping")
|
||||
def ping_remote(name: str | None = typer.Argument(None)):
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, name)
|
||||
remote_ping(store, name, console)
|
||||
except (ConfigError, ClientError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def login(
|
||||
remote: str | None = typer.Option(None, "--remote", help="Remote name."),
|
||||
browser: bool = typer.Option(False, "--browser", help="Use browser login."),
|
||||
api_key: str | None = typer.Option(None, "--api-key", help="Import an existing API Key."),
|
||||
no_open: bool = typer.Option(False, "--no-open", help="Print browser URL without opening it."),
|
||||
):
|
||||
if browser and api_key:
|
||||
_handle_error(CommandError("--browser 和 --api-key 不能同时使用"))
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
if api_key:
|
||||
login_with_api_key(store, remote, api_key, console)
|
||||
return
|
||||
if browser:
|
||||
login_with_browser(store, remote, no_open, console)
|
||||
return
|
||||
|
||||
mode = select_login_mode(console)
|
||||
if mode == "api_key":
|
||||
typed_key = typer.prompt("API Key", hide_input=True)
|
||||
login_with_api_key(store, remote, typed_key, console)
|
||||
else:
|
||||
login_with_browser(store, remote, no_open, console)
|
||||
except (ConfigError, ClientError, CommandError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def whoami(remote: str | None = typer.Option(None, "--remote", help="Remote name.")):
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
whoami_command(store, remote, console)
|
||||
except (ConfigError, ClientError, CommandError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def status(remote: str | None = typer.Option(None, "--remote", help="Remote name.")):
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
status_command(store, remote, console)
|
||||
except (ConfigError, ClientError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def logout(
|
||||
remote: str | None = typer.Option(None, "--remote", help="Remote name."),
|
||||
local_only: bool = typer.Option(False, "--local-only", help="Only remove local credentials."),
|
||||
):
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
logout_command(store, remote, local_only, console)
|
||||
except (ConfigError, ClientError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@kb_app.command("upload")
|
||||
def upload_knowledge_base_files(
|
||||
path: Path = typer.Argument(..., help="File or directory to upload."),
|
||||
kb_id: str | None = typer.Option(None, "--kb-id", help="Knowledge base ID. Prompt when omitted."),
|
||||
remote: str | None = typer.Option(None, "--remote", help="Remote name."),
|
||||
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
||||
concurrency: int = typer.Option(
|
||||
DEFAULT_CONCURRENCY,
|
||||
"--concurrency",
|
||||
help=f"Concurrent upload count, 1-{MAX_CONCURRENCY}.",
|
||||
),
|
||||
include_ext: str | None = typer.Option(None, "--include-ext", help="Comma separated extension allowlist."),
|
||||
exclude_ext: str | None = typer.Option(None, "--exclude-ext", help="Comma separated extension denylist."),
|
||||
force_upload_file: bool = typer.Option(
|
||||
False,
|
||||
"--force-upload-file",
|
||||
help="Skip remote filename existence check before upload.",
|
||||
),
|
||||
):
|
||||
options = KbUploadOptions(
|
||||
path=path,
|
||||
kb_id=kb_id,
|
||||
yes=yes,
|
||||
concurrency=concurrency,
|
||||
include_ext=include_ext,
|
||||
exclude_ext=exclude_ext,
|
||||
force_upload_file=force_upload_file,
|
||||
)
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
run_kb_upload(store, remote, options, console)
|
||||
except (ConfigError, ClientError, KbUploadError) as exc:
|
||||
_handle_error(exc)
|
||||
|
||||
|
||||
@agent_app.command("eval")
|
||||
def eval_agent(
|
||||
dataset_name: str = typer.Option(..., "--dataset-name", help="Langfuse dataset name."),
|
||||
agent_slug: str = typer.Option(..., "--agent-slug", help="Yuxi agent slug."),
|
||||
experiment_name: str | None = typer.Option(None, "--experiment-name", help="Langfuse experiment name."),
|
||||
remote: str | None = typer.Option(None, "--remote", help="Remote name."),
|
||||
max_concurrency: int = typer.Option(1, "--max-concurrency", help="Langfuse experiment max concurrency."),
|
||||
timeout_seconds: float = typer.Option(900, "--timeout-seconds", help="Per item Yuxi API timeout."),
|
||||
):
|
||||
options = AgentEvalOptions(
|
||||
dataset_name=dataset_name,
|
||||
agent_slug=agent_slug,
|
||||
experiment_name=experiment_name,
|
||||
max_concurrency=max_concurrency,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
store = _store()
|
||||
try:
|
||||
_print_remote_context(store, remote)
|
||||
run_langfuse_agent_experiment(store, remote, options, console)
|
||||
except (ConfigError, ClientError, AgentEvalError) as exc:
|
||||
_handle_error(exc)
|
||||
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from yuxi_cli.agent_eval import AgentEvalError, AgentEvalOptions, extract_query, run_langfuse_agent_experiment
|
||||
from yuxi_cli.config import ConfigStore, Remote
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def __init__(self, outputs: list[str]):
|
||||
self.item_results = outputs
|
||||
self.output = outputs[0] if outputs else ""
|
||||
|
||||
@classmethod
|
||||
def single(cls, output: str):
|
||||
return cls([output])
|
||||
|
||||
def format(self, *, include_item_results: bool) -> str:
|
||||
assert include_item_results is True
|
||||
return f"formatted: {self.output}"
|
||||
|
||||
|
||||
class FakeDataset:
|
||||
def __init__(self, item, *, result: FakeResult | None = None):
|
||||
self.item = item
|
||||
self.items = [item]
|
||||
self.result = result
|
||||
self.run_kwargs = None
|
||||
|
||||
def run_experiment(self, **kwargs):
|
||||
self.run_kwargs = kwargs
|
||||
output = kwargs["task"](item=self.item)
|
||||
return self.result or FakeResult.single(output)
|
||||
|
||||
|
||||
class FakePartialDataset:
|
||||
def __init__(self):
|
||||
self.items = [
|
||||
SimpleNamespace(id="item-1", input="hello"),
|
||||
SimpleNamespace(id="item-2", input="world"),
|
||||
]
|
||||
self.run_kwargs = None
|
||||
|
||||
def run_experiment(self, **kwargs):
|
||||
self.run_kwargs = kwargs
|
||||
kwargs["task"](item=self.items[0])
|
||||
return FakeResult.single("final answer")
|
||||
|
||||
|
||||
class FakeLangfuse:
|
||||
def __init__(self, dataset):
|
||||
self.dataset = dataset
|
||||
self.flushed = 0
|
||||
|
||||
def get_dataset(self, name: str):
|
||||
assert name == "agent-eval-smoke"
|
||||
return self.dataset
|
||||
|
||||
def flush(self):
|
||||
self.flushed += 1
|
||||
|
||||
|
||||
class FakeYuxiClient:
|
||||
calls = []
|
||||
|
||||
def __init__(self, remote: Remote, timeout: float = 30.0):
|
||||
self.remote = remote
|
||||
self.timeout = timeout
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc):
|
||||
return None
|
||||
|
||||
def run_agent_eval(self, **kwargs):
|
||||
self.calls.append({"remote": self.remote, "client_timeout": self.timeout, "kwargs": kwargs})
|
||||
return {"status": "completed", "output": "final answer"}
|
||||
|
||||
|
||||
def _console():
|
||||
return Console(file=io.StringIO(), force_terminal=False)
|
||||
|
||||
|
||||
def test_extract_query_supports_string_and_common_fields():
|
||||
assert extract_query("hello") == "hello"
|
||||
assert extract_query({"query": "hello"}) == "hello"
|
||||
assert extract_query({"question": "hello"}) == "hello"
|
||||
assert extract_query({"prompt": "hello"}) == "hello"
|
||||
|
||||
|
||||
def test_extract_query_rejects_unrecognized_input():
|
||||
with pytest.raises(AgentEvalError, match="无法从 Langfuse dataset item input 中提取 query"):
|
||||
extract_query({"text": "hello"})
|
||||
|
||||
|
||||
def test_run_langfuse_agent_experiment_uses_remote_api_key(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.get_remote("local")
|
||||
remote.api_key = "yxkey_local"
|
||||
store.save(config)
|
||||
dataset = FakeDataset(SimpleNamespace(id="item-1", input={"query": "2+2=?"}))
|
||||
langfuse = FakeLangfuse(dataset)
|
||||
console = _console()
|
||||
FakeYuxiClient.calls = []
|
||||
|
||||
run_langfuse_agent_experiment(
|
||||
store,
|
||||
None,
|
||||
AgentEvalOptions(
|
||||
dataset_name="agent-eval-smoke",
|
||||
agent_slug="default-chatbot",
|
||||
experiment_name="exp-1",
|
||||
max_concurrency=2,
|
||||
timeout_seconds=123,
|
||||
),
|
||||
console,
|
||||
langfuse_factory=lambda: langfuse,
|
||||
client_factory=FakeYuxiClient,
|
||||
)
|
||||
|
||||
assert dataset.run_kwargs["name"] == "exp-1"
|
||||
assert dataset.run_kwargs["max_concurrency"] == 2
|
||||
assert dataset.run_kwargs["metadata"] == {
|
||||
"source": "agent_evaluation",
|
||||
"agent_slug": "default-chatbot",
|
||||
"dataset_name": "agent-eval-smoke",
|
||||
"remote": "local",
|
||||
}
|
||||
call = FakeYuxiClient.calls[0]
|
||||
assert call["remote"].name == "local"
|
||||
assert call["client_timeout"] == 123
|
||||
assert call["kwargs"]["query"] == "2+2=?"
|
||||
assert call["kwargs"]["agent_slug"] == "default-chatbot"
|
||||
assert "api_key" not in call["kwargs"]
|
||||
assert call["kwargs"]["timeout_seconds"] == 123
|
||||
assert call["kwargs"]["evaluation"] == {
|
||||
"dataset_name": "agent-eval-smoke",
|
||||
"dataset_item_id": "item-1",
|
||||
"experiment_name": "exp-1",
|
||||
}
|
||||
assert call["kwargs"]["meta"]["request_id"].startswith("eval-")
|
||||
assert "formatted: final answer" in console.file.getvalue()
|
||||
assert langfuse.flushed == 1
|
||||
|
||||
|
||||
def test_run_langfuse_agent_experiment_requires_login(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
|
||||
with pytest.raises(AgentEvalError, match="remote 尚未登录"):
|
||||
run_langfuse_agent_experiment(
|
||||
store,
|
||||
None,
|
||||
AgentEvalOptions(dataset_name="agent-eval-smoke", agent_slug="default-chatbot"),
|
||||
_console(),
|
||||
langfuse_factory=lambda: FakeLangfuse(FakeDataset(SimpleNamespace(id="1", input="hello"))),
|
||||
client_factory=FakeYuxiClient,
|
||||
)
|
||||
|
||||
|
||||
def test_run_langfuse_agent_experiment_rejects_partial_langfuse_results(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
config.get_remote("local").api_key = "yxkey_local"
|
||||
store.save(config)
|
||||
langfuse = FakeLangfuse(FakePartialDataset())
|
||||
FakeYuxiClient.calls = []
|
||||
|
||||
with pytest.raises(AgentEvalError, match="1/2 个 item 成功写入"):
|
||||
run_langfuse_agent_experiment(
|
||||
store,
|
||||
None,
|
||||
AgentEvalOptions(
|
||||
dataset_name="agent-eval-smoke",
|
||||
agent_slug="default-chatbot",
|
||||
experiment_name="exp-1",
|
||||
),
|
||||
_console(),
|
||||
langfuse_factory=lambda: langfuse,
|
||||
client_factory=FakeYuxiClient,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from yuxi_cli.client import YuxiClient
|
||||
from yuxi_cli.config import Remote
|
||||
|
||||
|
||||
def test_run_agent_eval_uses_invocation_endpoint(monkeypatch):
|
||||
client = YuxiClient(Remote(name="local", url="http://localhost:5173", api_key="yxkey_test"))
|
||||
calls: dict[str, object] = {}
|
||||
|
||||
def fake_request(method, path, **kwargs):
|
||||
calls["method"] = method
|
||||
calls["path"] = path
|
||||
calls["kwargs"] = kwargs
|
||||
return {"status": "completed", "output": "final answer"}
|
||||
|
||||
monkeypatch.setattr(client, "_request", fake_request)
|
||||
|
||||
try:
|
||||
result = client.run_agent_eval(
|
||||
query="2+2=?",
|
||||
agent_slug="default-chatbot",
|
||||
evaluation={"dataset_name": "dataset-1"},
|
||||
meta={"request_id": "req-1"},
|
||||
timeout_seconds=123,
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
assert result == {"status": "completed", "output": "final answer"}
|
||||
assert calls["method"] == "POST"
|
||||
assert calls["path"] == "/agent-invocation/eval/runs"
|
||||
assert calls["kwargs"]["timeout"] == 123
|
||||
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from yuxi_cli.client import CLIAuthSession, ClientError
|
||||
from yuxi_cli.commands import CommandError, login_with_api_key, login_with_browser, logout
|
||||
from yuxi_cli.config import ConfigStore, Remote
|
||||
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, remote: Remote):
|
||||
self.remote = remote
|
||||
self.exchanges = 0
|
||||
self.deleted_api_key_id = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc):
|
||||
return None
|
||||
|
||||
def discovery(self):
|
||||
return {
|
||||
"version": "0.7.1",
|
||||
"capabilities": {
|
||||
"cli": {
|
||||
"browser_login": True,
|
||||
"api_key_auth": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def me(self, api_key=None):
|
||||
assert api_key == "yxkey_existing" or self.remote.api_key
|
||||
return {
|
||||
"id": 1,
|
||||
"uid": "admin",
|
||||
"username": "Admin",
|
||||
"role": "superadmin",
|
||||
}
|
||||
|
||||
def create_cli_session(self):
|
||||
return CLIAuthSession(
|
||||
device_code="yxcli_device",
|
||||
user_code="ABCD-EFGH",
|
||||
verification_uri="/auth/cli/authorize",
|
||||
expires_in=30,
|
||||
interval=1,
|
||||
)
|
||||
|
||||
def authorize_url(self, session):
|
||||
return f"{self.remote.url}{session.authorize_path}"
|
||||
|
||||
def exchange_cli_token(self, _device_code):
|
||||
self.exchanges += 1
|
||||
if self.exchanges == 1:
|
||||
raise ClientError("authorization_pending: 等待浏览器授权")
|
||||
return {
|
||||
"secret": "yxkey_browser",
|
||||
"api_key": {"id": 42},
|
||||
"user": {"id": 1, "uid": "admin", "username": "Admin", "role": "superadmin"},
|
||||
}
|
||||
|
||||
def delete_api_key(self, api_key_id):
|
||||
self.deleted_api_key_id = api_key_id
|
||||
return {"success": True}
|
||||
|
||||
|
||||
def _console():
|
||||
return Console(file=io.StringIO(), force_terminal=False)
|
||||
|
||||
|
||||
def test_login_with_api_key_saves_remote_credentials(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
|
||||
remote = login_with_api_key(store, None, "yxkey_existing", _console(), client_factory=FakeClient)
|
||||
|
||||
loaded = store.load().get_remote("local")
|
||||
assert remote.api_key == "yxkey_existing"
|
||||
assert loaded.api_key == "yxkey_existing"
|
||||
|
||||
|
||||
def test_login_with_browser_polls_until_token_and_saves_credentials(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
opened = []
|
||||
sleeps = []
|
||||
clock = {"value": 0}
|
||||
|
||||
def monotonic():
|
||||
clock["value"] += 1
|
||||
return clock["value"]
|
||||
|
||||
remote = login_with_browser(
|
||||
store,
|
||||
None,
|
||||
no_open=False,
|
||||
console=_console(),
|
||||
client_factory=FakeClient,
|
||||
open_browser=lambda url: opened.append(url) or True,
|
||||
sleep=lambda seconds: sleeps.append(seconds),
|
||||
monotonic=monotonic,
|
||||
)
|
||||
|
||||
assert opened == ["http://localhost:5173/auth/cli/authorize?user_code=ABCD-EFGH"]
|
||||
assert sleeps == [1]
|
||||
assert remote.api_key == "yxkey_browser"
|
||||
assert remote.api_key_id == "42"
|
||||
assert store.load().get_remote("local").api_key == "yxkey_browser"
|
||||
|
||||
|
||||
def test_login_rejects_unsupported_server_version(tmp_path):
|
||||
class OldServerClient(FakeClient):
|
||||
def discovery(self):
|
||||
return {
|
||||
"version": "0.7.0",
|
||||
"capabilities": {
|
||||
"cli": {
|
||||
"browser_login": True,
|
||||
"api_key_auth": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
|
||||
with pytest.raises(CommandError, match="低于 CLI 要求 0.7.1"):
|
||||
login_with_api_key(store, None, "yxkey_existing", _console(), client_factory=OldServerClient)
|
||||
|
||||
|
||||
def test_login_with_browser_retries_transient_errors(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
clock = {"value": 0}
|
||||
|
||||
class FlakyClient(FakeClient):
|
||||
def exchange_cli_token(self, _device_code):
|
||||
self.exchanges += 1
|
||||
if self.exchanges == 1:
|
||||
raise ClientError("请求远程失败: 连接被重置") # 网络层错误,status_code=None
|
||||
if self.exchanges == 2:
|
||||
raise ClientError("HTTP 503", status_code=503) # 服务端瞬时错误
|
||||
return {
|
||||
"secret": "yxkey_browser",
|
||||
"api_key": {"id": 42},
|
||||
"user": {"id": 1, "uid": "admin", "username": "Admin", "role": "superadmin"},
|
||||
}
|
||||
|
||||
remote = login_with_browser(
|
||||
store,
|
||||
None,
|
||||
no_open=True,
|
||||
console=_console(),
|
||||
client_factory=FlakyClient,
|
||||
open_browser=lambda url: True,
|
||||
sleep=lambda seconds: None,
|
||||
monotonic=lambda: clock.__setitem__("value", clock["value"] + 1) or clock["value"],
|
||||
)
|
||||
|
||||
assert remote.api_key == "yxkey_browser"
|
||||
|
||||
|
||||
def test_login_with_browser_aborts_on_terminal_error(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
clock = {"value": 0}
|
||||
|
||||
class ExpiredClient(FakeClient):
|
||||
def exchange_cli_token(self, _device_code):
|
||||
raise ClientError("expired_token: 授权会话已过期", error_code="expired_token", status_code=410)
|
||||
|
||||
with pytest.raises(ClientError, match="expired_token"):
|
||||
login_with_browser(
|
||||
store,
|
||||
None,
|
||||
no_open=True,
|
||||
console=_console(),
|
||||
client_factory=ExpiredClient,
|
||||
open_browser=lambda url: True,
|
||||
sleep=lambda seconds: None,
|
||||
monotonic=lambda: clock.__setitem__("value", clock["value"] + 1) or clock["value"],
|
||||
)
|
||||
|
||||
|
||||
def test_logout_clears_local_credentials(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.get_remote("local")
|
||||
remote.api_key = "yxkey_browser"
|
||||
remote.api_key_id = "42"
|
||||
store.save(config)
|
||||
|
||||
logout(store, None, local_only=True, console=_console(), client_factory=FakeClient)
|
||||
|
||||
loaded = store.load().get_remote("local")
|
||||
assert loaded.api_key == ""
|
||||
assert loaded.api_key_id == ""
|
||||
@@ -0,0 +1,69 @@
|
||||
from yuxi_cli.config import ConfigStore, normalize_remote_url
|
||||
|
||||
|
||||
def test_normalize_remote_url_adds_scheme_and_removes_api_suffix():
|
||||
assert normalize_remote_url("localhost:5173") == "http://localhost:5173"
|
||||
assert normalize_remote_url("https://example.com/api") == "https://example.com"
|
||||
assert normalize_remote_url("https://example.com/yuxi/api") == "https://example.com/yuxi"
|
||||
|
||||
|
||||
def test_config_store_uses_implicit_local_default(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
|
||||
assert config.current == "local"
|
||||
assert config.remotes["local"].url == "http://localhost:5173"
|
||||
|
||||
|
||||
def test_config_store_persists_remote_credentials(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.set_remote("prod", "https://example.com/api")
|
||||
remote.api_key = "yxkey_test"
|
||||
remote.api_key_id = "12"
|
||||
config.use_remote("prod")
|
||||
store.save(config)
|
||||
|
||||
loaded = store.load()
|
||||
assert loaded.current == "prod"
|
||||
assert loaded.remotes["prod"].url == "https://example.com"
|
||||
assert loaded.remotes["prod"].api_key == "yxkey_test"
|
||||
assert loaded.remotes["prod"].api_key_id == "12"
|
||||
|
||||
|
||||
def test_remote_url_change_clears_credentials(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.set_remote("prod", "https://one.example.com")
|
||||
remote.api_key = "yxkey_old"
|
||||
remote.api_key_id = "12"
|
||||
|
||||
config.set_remote("prod", "https://two.example.com")
|
||||
|
||||
assert config.remotes["prod"].api_key == ""
|
||||
assert config.remotes["prod"].api_key_id == ""
|
||||
|
||||
|
||||
def test_config_escapes_special_chars_in_remote_name(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
# remote 名称由用户输入,可能包含引号/反斜杠,需转义后仍能往返解析。
|
||||
name = 'pr"o\\d'
|
||||
config.set_remote(name, "https://example.com")
|
||||
config.use_remote(name)
|
||||
store.save(config)
|
||||
|
||||
loaded = store.load()
|
||||
assert loaded.current == name
|
||||
assert loaded.remotes[name].url == "https://example.com"
|
||||
|
||||
|
||||
def test_config_file_is_created_with_owner_only_permissions(tmp_path):
|
||||
import stat as stat_module
|
||||
|
||||
path = tmp_path / "config.toml"
|
||||
store = ConfigStore(path)
|
||||
store.save(store.load())
|
||||
|
||||
mode = stat_module.S_IMODE(path.stat().st_mode)
|
||||
assert mode == 0o600
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from yuxi_cli.discovery import ServerCompatibilityError, ensure_server_compatible, is_server_version_supported
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version", "expected"),
|
||||
[
|
||||
("0.7.1", True),
|
||||
("0.7.2", True),
|
||||
("0.7.1.post1", True),
|
||||
("0.7.1.dev0", True),
|
||||
("0.7.2.dev0", True),
|
||||
("0.7.0", False),
|
||||
("0.7.0.dev99", False),
|
||||
("0.7.1rc1", False),
|
||||
("unknown", False),
|
||||
],
|
||||
)
|
||||
def test_is_server_version_supported_handles_dev_releases(version, expected):
|
||||
assert is_server_version_supported(version) is expected
|
||||
|
||||
|
||||
def test_ensure_server_compatible_requires_capability():
|
||||
discovery = {
|
||||
"version": "0.7.1",
|
||||
"capabilities": {
|
||||
"cli": {
|
||||
"browser_login": False,
|
||||
"api_key_auth": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ServerCompatibilityError, match="cli.browser_login"):
|
||||
ensure_server_compatible(discovery, "cli.browser_login")
|
||||
@@ -0,0 +1,620 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import threading
|
||||
import time
|
||||
from collections import Counter
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
import yuxi_cli.kb_upload as kb_upload_module
|
||||
from yuxi_cli.client import ClientError
|
||||
from yuxi_cli.config import ConfigStore, Remote
|
||||
from yuxi_cli.kb_upload import (
|
||||
ALREADY_EXISTS_MESSAGE,
|
||||
ALREADY_UPLOADED_MESSAGE,
|
||||
DEFAULT_CONCURRENCY,
|
||||
ExtensionOption,
|
||||
KbUploadOptions,
|
||||
KbUploadError,
|
||||
KbUploadSummary,
|
||||
LocalFile,
|
||||
MAX_CONCURRENCY,
|
||||
SkippedFile,
|
||||
_database_choices,
|
||||
_format_unsupported_summary,
|
||||
_extension_choices,
|
||||
_extension_option_label,
|
||||
_print_selection_summary,
|
||||
run_kb_upload,
|
||||
upload_files,
|
||||
)
|
||||
|
||||
|
||||
class FakeKbClient:
|
||||
uploaded: list[str] = []
|
||||
add_payload: dict | None = None
|
||||
add_payloads: list[dict] = []
|
||||
events: list[tuple[str, str | list[str]]] = []
|
||||
exists_checks: list[str] = []
|
||||
existing_files: set[str] = set()
|
||||
active_uploads = 0
|
||||
max_active_uploads = 0
|
||||
lock = threading.Lock()
|
||||
|
||||
def __init__(self, remote: Remote):
|
||||
self.remote = remote
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def reset(cls) -> None:
|
||||
cls.uploaded = []
|
||||
cls.add_payload = None
|
||||
cls.add_payloads = []
|
||||
cls.events = []
|
||||
cls.exists_checks = []
|
||||
cls.existing_files = set()
|
||||
cls.active_uploads = 0
|
||||
cls.max_active_uploads = 0
|
||||
|
||||
def discovery(self):
|
||||
return {
|
||||
"version": "0.7.1",
|
||||
"capabilities": {"cli": {"kb_upload": True}},
|
||||
}
|
||||
|
||||
def get_database(self, kb_id: str):
|
||||
return {"kb_id": kb_id, "name": "Test KB", "kb_type": "milvus"}
|
||||
|
||||
def list_databases(self):
|
||||
return {
|
||||
"databases": [
|
||||
{"kb_id": "kb_1", "name": "Milvus KB", "kb_type": "milvus"},
|
||||
{"kb_id": "dify_1", "name": "Dify KB", "kb_type": "dify"},
|
||||
]
|
||||
}
|
||||
|
||||
def get_knowledge_base_types(self):
|
||||
return {
|
||||
"kb_types": {
|
||||
"milvus": {"supports_documents": True},
|
||||
"dify": {"supports_documents": False},
|
||||
}
|
||||
}
|
||||
|
||||
def get_supported_file_types(self):
|
||||
return {
|
||||
"file_types": [
|
||||
".bmp",
|
||||
".csv",
|
||||
".docx",
|
||||
".html",
|
||||
".htm",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".json",
|
||||
".md",
|
||||
".pdf",
|
||||
".png",
|
||||
".pptx",
|
||||
".tif",
|
||||
".tiff",
|
||||
".txt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
]
|
||||
}
|
||||
|
||||
def knowledge_document_exists(self, kb_id: str, filename: str) -> bool:
|
||||
with self.lock:
|
||||
type(self).exists_checks.append(filename)
|
||||
type(self).events.append(("exists", filename))
|
||||
return filename in type(self).existing_files
|
||||
|
||||
def upload_knowledge_file(self, kb_id: str, path: Path):
|
||||
with self.lock:
|
||||
type(self).active_uploads += 1
|
||||
type(self).max_active_uploads = max(type(self).max_active_uploads, type(self).active_uploads)
|
||||
try:
|
||||
time.sleep(0.02)
|
||||
with self.lock:
|
||||
type(self).uploaded.append(path.name)
|
||||
type(self).events.append(("upload", path.name))
|
||||
return {
|
||||
"file_path": f"minio://knowledgebases/{kb_id}/upload/{path.name}",
|
||||
"content_hash": f"hash-{path.name}",
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
finally:
|
||||
with self.lock:
|
||||
type(self).active_uploads -= 1
|
||||
|
||||
def add_uploaded_documents(self, kb_id: str, items: list[str], params: dict):
|
||||
payload = {"kb_id": kb_id, "items": list(items), "params": params}
|
||||
item_names = [item.rsplit("/", 1)[-1] for item in items]
|
||||
with self.lock:
|
||||
type(self).add_payloads.append(payload)
|
||||
type(self).events.append(("add", item_names))
|
||||
if type(self).add_payload is None:
|
||||
type(self).add_payload = {
|
||||
"kb_id": kb_id,
|
||||
"items": list(items),
|
||||
"params": {
|
||||
"content_type": params.get("content_type"),
|
||||
"content_hashes": dict(params.get("content_hashes") or {}),
|
||||
"file_sizes": dict(params.get("file_sizes") or {}),
|
||||
"source_paths": dict(params.get("source_paths") or {}),
|
||||
},
|
||||
}
|
||||
else:
|
||||
type(self).add_payload["items"].extend(items)
|
||||
type(self).add_payload["params"]["content_hashes"].update(params.get("content_hashes") or {})
|
||||
type(self).add_payload["params"]["file_sizes"].update(params.get("file_sizes") or {})
|
||||
type(self).add_payload["params"]["source_paths"].update(params.get("source_paths") or {})
|
||||
return {"status": "success", "added": len(items), "failed": 0, "items": [], "failed_items": []}
|
||||
|
||||
|
||||
def _console():
|
||||
return Console(file=io.StringIO(), force_terminal=False)
|
||||
|
||||
|
||||
def _store(tmp_path: Path) -> ConfigStore:
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.get_remote("local")
|
||||
remote.api_key = "yxkey_test"
|
||||
store.save(config)
|
||||
return store
|
||||
|
||||
|
||||
def test_kb_upload_default_include_excludes_structured_and_presentation_files(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
for name in [
|
||||
"a.md",
|
||||
"b.txt",
|
||||
"c.docx",
|
||||
"d.html",
|
||||
"e.htm",
|
||||
"f.json",
|
||||
"g.csv",
|
||||
"h.xls",
|
||||
"i.xlsx",
|
||||
"j.pptx",
|
||||
"k.pdf",
|
||||
]:
|
||||
(tmp_path / name).write_text("demo", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert sorted(FakeKbClient.uploaded) == ["a.md", "b.txt", "c.docx", "d.html", "e.htm"]
|
||||
assert FakeKbClient.add_payload is not None
|
||||
assert len(FakeKbClient.add_payload["items"]) == 5
|
||||
|
||||
|
||||
def test_kb_upload_default_concurrency_is_10():
|
||||
assert KbUploadOptions(path=Path(".")).concurrency == DEFAULT_CONCURRENCY == 10
|
||||
|
||||
|
||||
def test_kb_upload_allows_concurrency_300(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=MAX_CONCURRENCY),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.uploaded == ["note.md"]
|
||||
|
||||
|
||||
def test_kb_upload_rejects_concurrency_above_300(tmp_path):
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
with pytest.raises(KbUploadError, match="1 到 300"):
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=MAX_CONCURRENCY + 1),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
|
||||
def test_kb_upload_preserves_relative_source_paths(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
docs_dir = tmp_path / "docs" / "guide"
|
||||
docs_dir.mkdir(parents=True)
|
||||
(docs_dir / "intro.md").write_text("intro", encoding="utf-8")
|
||||
(tmp_path / "root.txt").write_text("root", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.add_payload is not None
|
||||
source_paths = FakeKbClient.add_payload["params"]["source_paths"]
|
||||
assert source_paths == {
|
||||
"minio://knowledgebases/kb_1/upload/intro.md": "docs/guide/intro.md",
|
||||
"minio://knowledgebases/kb_1/upload/root.txt": "root.txt",
|
||||
}
|
||||
|
||||
|
||||
def test_kb_upload_without_kb_id_selects_only_uploadable_database(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.add_payload is not None
|
||||
assert FakeKbClient.add_payload["kb_id"] == "kb_1"
|
||||
assert FakeKbClient.add_payload["items"] == ["minio://knowledgebases/kb_1/upload/note.md"]
|
||||
|
||||
|
||||
def test_kb_upload_include_ext_allows_non_default_supported_types(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "data.xlsx").write_text("demo", encoding="utf-8")
|
||||
(tmp_path / "slides.pptx").write_text("demo", encoding="utf-8")
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2, include_ext="xlsx,pptx"),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert sorted(FakeKbClient.uploaded) == ["data.xlsx", "slides.pptx"]
|
||||
|
||||
|
||||
def test_kb_upload_limits_upload_concurrency(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
for index in range(6):
|
||||
(tmp_path / f"{index}.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.max_active_uploads <= 2
|
||||
assert FakeKbClient.max_active_uploads > 1
|
||||
|
||||
|
||||
def test_upload_files_limits_pending_submissions(monkeypatch, tmp_path):
|
||||
FakeKbClient.reset()
|
||||
files = []
|
||||
for index in range(5):
|
||||
path = tmp_path / f"{index}.md"
|
||||
path.write_text("demo", encoding="utf-8")
|
||||
files.append(LocalFile(path, path.name, ".md", path.stat().st_size))
|
||||
|
||||
state = {"wait_started": False, "submitted_before_wait": 0}
|
||||
|
||||
class TrackingExecutor:
|
||||
def __init__(self, max_workers: int):
|
||||
self.max_workers = max_workers
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc):
|
||||
return None
|
||||
|
||||
def submit(self, fn, item):
|
||||
if not state["wait_started"]:
|
||||
state["submitted_before_wait"] += 1
|
||||
future = Future()
|
||||
future.set_result(fn(item))
|
||||
return future
|
||||
|
||||
def tracking_wait(pending, *, return_when):
|
||||
assert return_when == kb_upload_module.FIRST_COMPLETED
|
||||
state["wait_started"] = True
|
||||
completed = {next(iter(pending))}
|
||||
return completed, set(pending) - completed
|
||||
|
||||
monkeypatch.setattr(kb_upload_module, "ThreadPoolExecutor", TrackingExecutor)
|
||||
monkeypatch.setattr(kb_upload_module, "wait", tracking_wait)
|
||||
|
||||
uploaded, failed, add_response = upload_files(
|
||||
Remote(name="local", url="http://localhost", api_key="yxkey_test"),
|
||||
FakeKbClient,
|
||||
"kb_1",
|
||||
files,
|
||||
concurrency=2,
|
||||
console=_console(),
|
||||
)
|
||||
|
||||
assert len(uploaded) == 5
|
||||
assert failed == []
|
||||
assert add_response is not None
|
||||
assert state["submitted_before_wait"] == 2
|
||||
|
||||
|
||||
def test_kb_upload_treats_duplicate_content_as_already_uploaded(tmp_path):
|
||||
class DuplicateContentClient(FakeKbClient):
|
||||
def upload_knowledge_file(self, kb_id: str, path: Path):
|
||||
raise ClientError(ALREADY_UPLOADED_MESSAGE, status_code=400)
|
||||
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
buffer = io.StringIO()
|
||||
console = Console(file=buffer, force_terminal=False)
|
||||
|
||||
summary = run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
console,
|
||||
client_factory=DuplicateContentClient,
|
||||
)
|
||||
|
||||
assert summary.uploaded == []
|
||||
assert summary.already_uploaded_count == 1
|
||||
assert summary.real_upload_failed_count == 0
|
||||
assert summary.add_failed_count == 0
|
||||
output = buffer.getvalue()
|
||||
assert "已上传过: 1" in output
|
||||
assert "上传失败: 0" in output
|
||||
|
||||
|
||||
def test_kb_upload_skips_existing_relative_path_before_upload(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
docs_dir = tmp_path / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "existing.md").write_text("existing", encoding="utf-8")
|
||||
(docs_dir / "new.md").write_text("new", encoding="utf-8")
|
||||
FakeKbClient.existing_files = {"docs/existing.md"}
|
||||
|
||||
summary = run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert sorted(FakeKbClient.exists_checks) == ["docs/existing.md", "docs/new.md"]
|
||||
assert FakeKbClient.uploaded == ["new.md"]
|
||||
assert FakeKbClient.add_payload is not None
|
||||
assert FakeKbClient.add_payload["params"]["source_paths"] == {
|
||||
"minio://knowledgebases/kb_1/upload/new.md": "docs/new.md"
|
||||
}
|
||||
assert summary.already_uploaded_count == 1
|
||||
assert summary.upload_failed[0].error == ALREADY_EXISTS_MESSAGE
|
||||
assert ("upload", "existing.md") not in FakeKbClient.events
|
||||
|
||||
|
||||
def test_kb_upload_falls_back_to_upload_when_exists_check_fails(tmp_path):
|
||||
class ExistsCheckFailedClient(FakeKbClient):
|
||||
def knowledge_document_exists(self, kb_id: str, filename: str) -> bool:
|
||||
with self.lock:
|
||||
type(self).exists_checks.append(filename)
|
||||
type(self).events.append(("exists-error", filename))
|
||||
raise ClientError("exists endpoint unavailable", status_code=404)
|
||||
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
summary = run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=ExistsCheckFailedClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.exists_checks == ["note.md"]
|
||||
assert FakeKbClient.uploaded == ["note.md"]
|
||||
assert summary.already_uploaded_count == 0
|
||||
assert summary.add_response is not None
|
||||
assert summary.add_response["added"] == 1
|
||||
|
||||
|
||||
def test_kb_upload_force_upload_file_skips_exists_check(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
(tmp_path / "note.md").write_text("demo", encoding="utf-8")
|
||||
FakeKbClient.existing_files = {"note.md"}
|
||||
|
||||
summary = run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2, force_upload_file=True),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
assert FakeKbClient.exists_checks == []
|
||||
assert FakeKbClient.uploaded == ["note.md"]
|
||||
assert summary.already_uploaded_count == 0
|
||||
assert summary.add_response is not None
|
||||
assert summary.add_response["added"] == 1
|
||||
|
||||
|
||||
def test_kb_upload_adds_each_document_after_its_upload(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
for index in range(5):
|
||||
(tmp_path / f"{index}.md").write_text("demo", encoding="utf-8")
|
||||
|
||||
summary = run_kb_upload(
|
||||
_store(tmp_path),
|
||||
None,
|
||||
KbUploadOptions(path=tmp_path, kb_id="kb_1", yes=True, concurrency=2),
|
||||
_console(),
|
||||
client_factory=FakeKbClient,
|
||||
)
|
||||
|
||||
added_names = [[item.rsplit("/", 1)[-1] for item in payload["items"]] for payload in FakeKbClient.add_payloads]
|
||||
assert all(len(items) == 1 for items in added_names)
|
||||
assert sorted(item for items in added_names for item in items) == [f"{index}.md" for index in range(5)]
|
||||
for index in range(5):
|
||||
name = f"{index}.md"
|
||||
upload_index = FakeKbClient.events.index(("upload", name))
|
||||
add_index = FakeKbClient.events.index(("add", [name]))
|
||||
assert upload_index < add_index
|
||||
assert summary.add_response is not None
|
||||
assert summary.add_response["added"] == 5
|
||||
|
||||
|
||||
def test_upload_files_uses_log_progress_for_non_tty_console(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
files = []
|
||||
for index in range(3):
|
||||
path = tmp_path / f"{index}.md"
|
||||
path.write_text("demo", encoding="utf-8")
|
||||
files.append(LocalFile(path, path.name, ".md", path.stat().st_size))
|
||||
buffer = io.StringIO()
|
||||
console = Console(file=buffer, force_terminal=False)
|
||||
|
||||
uploaded, failed, add_response = upload_files(
|
||||
Remote(name="local", url="http://localhost", api_key="yxkey_test"),
|
||||
FakeKbClient,
|
||||
"kb_1",
|
||||
files,
|
||||
concurrency=2,
|
||||
console=console,
|
||||
)
|
||||
|
||||
output = buffer.getvalue()
|
||||
assert len(uploaded) == 3
|
||||
assert failed == []
|
||||
assert add_response is not None
|
||||
assert add_response["added"] == 3
|
||||
assert "处理进度: 3/3" in output
|
||||
assert "✓" not in output
|
||||
|
||||
|
||||
def test_upload_files_uses_progress_bar_for_tty_console(tmp_path):
|
||||
FakeKbClient.reset()
|
||||
files = []
|
||||
for index in range(3):
|
||||
path = tmp_path / f"{index}.md"
|
||||
path.write_text("demo", encoding="utf-8")
|
||||
files.append(LocalFile(path, path.name, ".md", path.stat().st_size))
|
||||
buffer = io.StringIO()
|
||||
console = Console(file=buffer, force_terminal=True, color_system=None, width=100)
|
||||
|
||||
uploaded, failed, add_response = upload_files(
|
||||
Remote(name="local", url="http://localhost", api_key="yxkey_test"),
|
||||
FakeKbClient,
|
||||
"kb_1",
|
||||
files,
|
||||
concurrency=2,
|
||||
console=console,
|
||||
)
|
||||
|
||||
output = buffer.getvalue()
|
||||
assert len(uploaded) == 3
|
||||
assert failed == []
|
||||
assert add_response is not None
|
||||
assert add_response["added"] == 3
|
||||
assert "处理进度" in output
|
||||
assert "处理进度:" not in output
|
||||
assert "0.md" not in output
|
||||
|
||||
|
||||
def test_database_choices_use_labels_without_numbering():
|
||||
choices = _database_choices(
|
||||
[
|
||||
{"kb_id": "kb_1", "name": "Alpha", "kb_type": "milvus"},
|
||||
{"kb_id": "kb_2", "name": "Beta", "kb_type": "milvus"},
|
||||
]
|
||||
)
|
||||
|
||||
assert choices[0].title == "Alpha [milvus] kb_1"
|
||||
assert choices[0].value == 0
|
||||
assert choices[1].title == "Beta [milvus] kb_2"
|
||||
assert choices[1].value == 1
|
||||
assert "1" not in str(choices[0].title).split("Alpha", 1)[0]
|
||||
|
||||
|
||||
def test_extension_choices_show_counts_and_default_selection():
|
||||
choices = _extension_choices(
|
||||
[
|
||||
ExtensionOption(".html", 101),
|
||||
ExtensionOption(".md", 165),
|
||||
ExtensionOption(".txt", 2),
|
||||
ExtensionOption(".json", 9),
|
||||
],
|
||||
selected_extensions={".html", ".md", ".txt"},
|
||||
)
|
||||
|
||||
assert [choice.title for choice in choices] == ["html (101)", "md (165)", "txt (2)", "json (9)"]
|
||||
assert [choice.value for choice in choices] == [".html", ".md", ".txt", ".json"]
|
||||
assert [choice.checked for choice in choices] == [True, True, True, False]
|
||||
assert _extension_option_label(ExtensionOption(".tar.gz", 3)) == "tar.gz (3)"
|
||||
|
||||
|
||||
def test_unsupported_summary_truncates_extensions_without_per_extension_counts():
|
||||
summary = _format_unsupported_summary(
|
||||
Counter(
|
||||
{
|
||||
".py": 100,
|
||||
".json": 90,
|
||||
".js": 80,
|
||||
".ts": 70,
|
||||
".map": 60,
|
||||
".css": 50,
|
||||
".yaml": 40,
|
||||
".lock": 30,
|
||||
".mjs": 20,
|
||||
".cjs": 10,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assert summary == "不支持: 550 (.py, .json, .js, .ts, .map, .css, .yaml, .lock, 等 2 类)"
|
||||
assert ".py 100" not in summary
|
||||
assert ".json 90" not in summary
|
||||
|
||||
|
||||
def test_selection_summary_shows_compact_selected_type_summary(tmp_path):
|
||||
selected = [LocalFile(tmp_path / "a.md", "a.md", ".md", 4)]
|
||||
skipped = [
|
||||
SkippedFile(tmp_path / "b.json", "b.json", "not-included"),
|
||||
SkippedFile(tmp_path / "c.py", "c.py", "unsupported"),
|
||||
]
|
||||
buffer = io.StringIO()
|
||||
console = Console(file=buffer, force_terminal=False)
|
||||
|
||||
_print_selection_summary(KbUploadSummary(scanned=3, selected=selected, skipped=skipped), console)
|
||||
|
||||
output = buffer.getvalue()
|
||||
assert " 扫描文件: 3" in output
|
||||
assert " 将上传: 1 (.md)" in output
|
||||
assert " 未选择: 1" in output
|
||||
assert " 不支持: 1 (.py)" in output
|
||||
assert "文件类型:" not in output
|
||||
assert "[x]" not in output
|
||||
assert "[ ]" not in output
|
||||
@@ -0,0 +1,53 @@
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from yuxi_cli import __version__
|
||||
from yuxi_cli.config import ConfigStore
|
||||
from yuxi_cli.main import app
|
||||
|
||||
|
||||
def test_version_option_without_command():
|
||||
result = CliRunner().invoke(app, ["--version"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert __version__ in result.output
|
||||
|
||||
|
||||
def test_agent_eval_help_is_registered():
|
||||
result = CliRunner().invoke(app, ["agent", "eval", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--dataset-name" in result.output
|
||||
assert "--create-smoke-item" not in result.output
|
||||
assert "--auth-token" not in result.output
|
||||
|
||||
|
||||
def test_kb_upload_help_is_registered():
|
||||
result = CliRunner().invoke(app, ["kb", "upload", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--kb-id" in result.output
|
||||
assert "--concurrency" in result.output
|
||||
assert "--force-upload-file" in result.output
|
||||
assert "1-300" in result.output
|
||||
|
||||
|
||||
def test_remote_command_prints_version_and_remote_context_first(tmp_path, monkeypatch):
|
||||
store = ConfigStore(tmp_path / "config.toml")
|
||||
config = store.load()
|
||||
remote = config.get_remote("local")
|
||||
remote.url = "https://example.com"
|
||||
store.save(config)
|
||||
|
||||
def fake_remote_ping(store_arg, name, console):
|
||||
assert store_arg is store
|
||||
assert name is None
|
||||
console.print("pong")
|
||||
|
||||
monkeypatch.setattr("yuxi_cli.main._store", lambda: store)
|
||||
monkeypatch.setattr("yuxi_cli.main.remote_ping", fake_remote_ping)
|
||||
|
||||
result = CliRunner().invoke(app, ["remote", "ping"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
lines = [line.strip() for line in result.output.splitlines() if line.strip()]
|
||||
assert lines[:3] == [f"Yuxi CLI {__version__}", "Remote: local https://example.com", "pong"]
|
||||
Generated
+680
@@ -0,0 +1,680 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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 = "googleapis-common-protos"
|
||||
version = "1.75.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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 = "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 = "langfuse"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
{ name = "httpx" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/ba/3a969cad28bcef7f42e0fd052df4cf54004268a3a405108e3e2e03055678/langfuse-4.12.0.tar.gz", hash = "sha256:af4c12ae61f69726e0ac73d14a4ada1a119013d5558598828d382f2a238d7314", size = 348143, upload-time = "2026-06-25T11:57:12.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f3/da8f6972566cfb9eb701a0d6a48668ab3e5ba6ae4ffeabb85e24a10e02ac/langfuse-4.12.0-py3-none-any.whl", hash = "sha256:55f6231d1e64dc6e9debe776cb3f06eb93ca7afb51322ce54571fa7493ac05c9", size = 609201, upload-time = "2026-06-25T11:57:10.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.43.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.43.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197, upload-time = "2026-06-24T15:20:00.247Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048, upload-time = "2026-06-24T15:19:41.264Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.43.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common" },
|
||||
{ name = "opentelemetry-proto" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/92/0b9f56412483a8891d4843890294796c9df8ab42417bd9bad8035d840cb3/opentelemetry_exporter_otlp_proto_http-1.43.0.tar.gz", hash = "sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f", size = 25406, upload-time = "2026-06-24T15:20:01.515Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/20/b685ed7af2e17c29ffc8af56f1fa8bc2033258fc30fb0d2b722f49d13ba0/opentelemetry_exporter_otlp_proto_http-1.43.0-py3-none-any.whl", hash = "sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610", size = 21795, upload-time = "2026-06-24T15:19:43.164Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.43.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481, upload-time = "2026-06-24T15:20:07.625Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489, upload-time = "2026-06-24T15:19:51.164Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.43.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.64b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" },
|
||||
]
|
||||
|
||||
[[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 = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "7.35.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" },
|
||||
]
|
||||
|
||||
[[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/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/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" },
|
||||
]
|
||||
|
||||
[[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 = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
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 = "questionary"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.26.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.8.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "2.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yuxi-cli"
|
||||
version = "0.1.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "packaging" },
|
||||
{ name = "questionary" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27" },
|
||||
{ name = "langfuse", specifier = ">=4.0.0" },
|
||||
{ name = "packaging", specifier = ">=24" },
|
||||
{ name = "questionary", specifier = ">=2.1.0" },
|
||||
{ name = "rich", specifier = ">=13.7" },
|
||||
{ name = "typer", specifier = ">=0.16" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
test = [{ name = "pytest", specifier = ">=8" }]
|
||||
Reference in New Issue
Block a user