Commit ·
a1231c7
1
Parent(s): 828386c
feat(main): finish data struct process
Browse files- open_cortex/runtime/client.py +1 -1
- open_cortex/ui/__init__.py +0 -0
- open_cortex/ui/app.py +0 -0
- open_cortex/ui/gradio_history.py +46 -0
- pyproject.toml +9 -4
- scripts/gradio_chat.py +127 -0
- tests/runtime/test_metrics.py +21 -0
- tests/ui/test_gradio_history.py +55 -0
- uv.lock +42 -0
open_cortex/runtime/client.py
CHANGED
|
@@ -17,7 +17,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 17 |
request_body = {
|
| 18 |
"messages": to_llama_messages(message),
|
| 19 |
"temperature": 0.2,
|
| 20 |
-
"max_tokens":
|
| 21 |
"stream": True,
|
| 22 |
"stream_options": {"include_usage": True},
|
| 23 |
"timings_per_token": True,
|
|
|
|
| 17 |
request_body = {
|
| 18 |
"messages": to_llama_messages(message),
|
| 19 |
"temperature": 0.2,
|
| 20 |
+
"max_tokens": 1024,
|
| 21 |
"stream": True,
|
| 22 |
"stream_options": {"include_usage": True},
|
| 23 |
"timings_per_token": True,
|
open_cortex/ui/__init__.py
ADDED
|
File without changes
|
open_cortex/ui/app.py
ADDED
|
File without changes
|
open_cortex/ui/gradio_history.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from open_cortex.runtime.messages import ChatMessage
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def normalize_gradio_content(content) -> str | None:
|
| 5 |
+
if isinstance(content, str):
|
| 6 |
+
return content
|
| 7 |
+
|
| 8 |
+
if isinstance(content, list):
|
| 9 |
+
parts = []
|
| 10 |
+
|
| 11 |
+
for item in content:
|
| 12 |
+
if isinstance(item, dict) and isinstance(item.get("text"), str):
|
| 13 |
+
parts.append(item["text"])
|
| 14 |
+
elif isinstance(item, str):
|
| 15 |
+
parts.append(item)
|
| 16 |
+
|
| 17 |
+
return "\n".join(parts)
|
| 18 |
+
|
| 19 |
+
if isinstance(content, dict) and isinstance(content.get("text"), str):
|
| 20 |
+
return content["text"]
|
| 21 |
+
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def history_to_chat_messages(history: list) -> list[ChatMessage]:
|
| 26 |
+
messages = []
|
| 27 |
+
|
| 28 |
+
for item in history:
|
| 29 |
+
if isinstance(item, dict):
|
| 30 |
+
role = item.get("role")
|
| 31 |
+
content = normalize_gradio_content(item.get("content"))
|
| 32 |
+
|
| 33 |
+
if role in {"system", "user", "assistant"} and content and content.strip():
|
| 34 |
+
messages.append(ChatMessage(role=role, content=content.strip()))
|
| 35 |
+
|
| 36 |
+
elif isinstance(item, (list, tuple)) and len(item) == 2:
|
| 37 |
+
user_content = normalize_gradio_content(item[0])
|
| 38 |
+
assistant_content = normalize_gradio_content(item[1])
|
| 39 |
+
|
| 40 |
+
if user_content and user_content.strip():
|
| 41 |
+
messages.append(ChatMessage(role="user", content=user_content.strip()))
|
| 42 |
+
|
| 43 |
+
if assistant_content and assistant_content.strip():
|
| 44 |
+
messages.append(ChatMessage(role="assistant", content=assistant_content.strip()))
|
| 45 |
+
|
| 46 |
+
return messages
|
pyproject.toml
CHANGED
|
@@ -9,10 +9,15 @@ dependencies = [
|
|
| 9 |
"httpx>=0.28.1",
|
| 10 |
]
|
| 11 |
|
| 12 |
-
[[tool.uv.index]]
|
| 13 |
-
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
| 14 |
-
default = true
|
| 15 |
-
|
| 16 |
[build-system]
|
| 17 |
requires = ["hatchling"]
|
| 18 |
build-backend = "hatchling.build"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"httpx>=0.28.1",
|
| 10 |
]
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
[build-system]
|
| 13 |
requires = ["hatchling"]
|
| 14 |
build-backend = "hatchling.build"
|
| 15 |
+
|
| 16 |
+
[dependency-groups]
|
| 17 |
+
dev = [
|
| 18 |
+
"pytest>=9.1.0",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[[tool.uv.index]]
|
| 22 |
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
| 23 |
+
default = true
|
scripts/gradio_chat.py
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from open_cortex.ui.gradio_history import history_to_chat_messages
|
| 3 |
+
from open_cortex.runtime.client import stream_chat_events
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def format_runtime(event) -> str:
|
| 7 |
+
if event.kind == "request_started":
|
| 8 |
+
return "Phase: request started\nWaiting for first token..."
|
| 9 |
+
|
| 10 |
+
if event.kind == "first_token" and event.snapshot is not None:
|
| 11 |
+
snapshot = event.snapshot
|
| 12 |
+
context_tokens = (
|
| 13 |
+
snapshot.slot_context_tokens[0]
|
| 14 |
+
if snapshot.slot_context_tokens
|
| 15 |
+
else None
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
return "\n".join(
|
| 19 |
+
[
|
| 20 |
+
"Phase: first token",
|
| 21 |
+
f"TTFT: {event.ttft_ms:.1f} ms",
|
| 22 |
+
f"Context: {context_tokens} / {snapshot.slot_context_size}",
|
| 23 |
+
f"Token Stream: {snapshot.decode_tps} tok/s",
|
| 24 |
+
(
|
| 25 |
+
"Engine: "
|
| 26 |
+
f"processing={snapshot.requests_processing} "
|
| 27 |
+
f"deferred={snapshot.requests_deferred}"
|
| 28 |
+
),
|
| 29 |
+
]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
if event.kind == "request_completed":
|
| 33 |
+
return "\n".join(
|
| 34 |
+
[
|
| 35 |
+
"Phase: completed",
|
| 36 |
+
f"Prompt tokens: {event.prompt_tokens}",
|
| 37 |
+
f"Output tokens: {event.completion_tokens}",
|
| 38 |
+
f"Prefill: {event.prompt_tps:.1f} tok/s",
|
| 39 |
+
f"Decode: {event.decode_tps:.1f} tok/s",
|
| 40 |
+
]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return "Phase: decoding"
|
| 44 |
+
|
| 45 |
+
def user(user_message: str, history: list[dict]) -> tuple[str, list[dict]]:
|
| 46 |
+
if not user_message.strip():
|
| 47 |
+
return "", history
|
| 48 |
+
|
| 49 |
+
return "", history + [
|
| 50 |
+
{
|
| 51 |
+
"role": "user",
|
| 52 |
+
"content": user_message,
|
| 53 |
+
}
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
def bot(history: list):
|
| 57 |
+
messages = history_to_chat_messages(history)
|
| 58 |
+
|
| 59 |
+
history.append(
|
| 60 |
+
{
|
| 61 |
+
"role": "assistant",
|
| 62 |
+
"content": "",
|
| 63 |
+
}
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
history.append(
|
| 67 |
+
{
|
| 68 |
+
"role": "assistant",
|
| 69 |
+
"content": "",
|
| 70 |
+
}
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
runtime_text = "Phase: idle"
|
| 74 |
+
|
| 75 |
+
for event in stream_chat_events(messages):
|
| 76 |
+
runtime_text = format_runtime(event)
|
| 77 |
+
|
| 78 |
+
if event.text_delta:
|
| 79 |
+
history[-1]["content"] += event.text_delta
|
| 80 |
+
|
| 81 |
+
yield history, runtime_text
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
with gr.Blocks() as demo:
|
| 85 |
+
gr.Markdown("# OpenCortex Minimal Chat")
|
| 86 |
+
|
| 87 |
+
with gr.Row():
|
| 88 |
+
with gr.Column(scale=2):
|
| 89 |
+
chatbot = gr.Chatbot(
|
| 90 |
+
height=480,
|
| 91 |
+
)
|
| 92 |
+
msg = gr.Textbox(
|
| 93 |
+
placeholder="Ask the local model...",
|
| 94 |
+
show_label=False,
|
| 95 |
+
)
|
| 96 |
+
clear = gr.Button("Clear")
|
| 97 |
+
|
| 98 |
+
with gr.Column(scale=1):
|
| 99 |
+
runtime = gr.Textbox(
|
| 100 |
+
label="Runtime",
|
| 101 |
+
value="Phase: idle",
|
| 102 |
+
lines=10,
|
| 103 |
+
interactive=False,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
msg.submit(
|
| 108 |
+
user,
|
| 109 |
+
[msg, chatbot],
|
| 110 |
+
[msg, chatbot],
|
| 111 |
+
queue=False,
|
| 112 |
+
).then(
|
| 113 |
+
bot,
|
| 114 |
+
chatbot,
|
| 115 |
+
[chatbot,runtime],
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
clear.click(
|
| 119 |
+
lambda: ([],"Phase: idle"),
|
| 120 |
+
None,
|
| 121 |
+
[chatbot, runtime],
|
| 122 |
+
queue=False,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
demo.queue()
|
| 127 |
+
demo.launch()
|
tests/runtime/test_metrics.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from open_cortex.runtime.metrics import parse_prometheus_value
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_parse_prometheus_value_reads_single_metric():
|
| 5 |
+
text = """
|
| 6 |
+
# HELP llamacpp:requests_processing Number of requests processing.
|
| 7 |
+
# TYPE llamacpp:requests_processing gauge
|
| 8 |
+
llamacpp:requests_processing 1
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
value = parse_prometheus_value(text, "llamacpp:requests_processing")
|
| 12 |
+
|
| 13 |
+
assert value == 1.0
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_parse_prometheus_value_returns_none_when_missing():
|
| 17 |
+
text = "llamacpp:requests_processing 1\n"
|
| 18 |
+
|
| 19 |
+
value = parse_prometheus_value(text, "llamacpp:requests_deferred")
|
| 20 |
+
|
| 21 |
+
assert value is None
|
tests/ui/test_gradio_history.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from open_cortex.ui.gradio_history import history_to_chat_messages
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_history_to_chat_messages_extracts_text_from_gradio_list_content():
|
| 5 |
+
history = [
|
| 6 |
+
{
|
| 7 |
+
"role": "user",
|
| 8 |
+
"content": [
|
| 9 |
+
{
|
| 10 |
+
"text": "请解释 KV Cache",
|
| 11 |
+
"type": "text",
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
messages = history_to_chat_messages(history)
|
| 18 |
+
|
| 19 |
+
assert len(messages) == 1
|
| 20 |
+
assert messages[0].role == "user"
|
| 21 |
+
assert messages[0].content == "请解释 KV Cache"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_history_to_chat_messages_ignores_non_text_content():
|
| 25 |
+
history = [
|
| 26 |
+
{
|
| 27 |
+
"role": "user",
|
| 28 |
+
"content": [
|
| 29 |
+
{
|
| 30 |
+
"path": "/tmp/image.png",
|
| 31 |
+
"type": "image",
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
}
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
messages = history_to_chat_messages(history)
|
| 38 |
+
|
| 39 |
+
assert messages == []
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_history_to_chat_messages_supports_legacy_pair_format():
|
| 43 |
+
history = [
|
| 44 |
+
[
|
| 45 |
+
"hello",
|
| 46 |
+
"hi",
|
| 47 |
+
]
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
messages = history_to_chat_messages(history)
|
| 51 |
+
|
| 52 |
+
assert [(m.role, m.content) for m in messages] == [
|
| 53 |
+
("user", "hello"),
|
| 54 |
+
("assistant", "hi"),
|
| 55 |
+
]
|
uv.lock
CHANGED
|
@@ -378,6 +378,15 @@ wheels = [
|
|
| 378 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
| 379 |
]
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
[[package]]
|
| 382 |
name = "jinja2"
|
| 383 |
version = "3.1.6"
|
|
@@ -544,12 +553,20 @@ dependencies = [
|
|
| 544 |
{ name = "httpx" },
|
| 545 |
]
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
[package.metadata]
|
| 548 |
requires-dist = [
|
| 549 |
{ name = "gradio", specifier = ">=6.18.0" },
|
| 550 |
{ name = "httpx", specifier = ">=0.28.1" },
|
| 551 |
]
|
| 552 |
|
|
|
|
|
|
|
|
|
|
| 553 |
[[package]]
|
| 554 |
name = "orjson"
|
| 555 |
version = "3.11.9"
|
|
@@ -733,6 +750,15 @@ wheels = [
|
|
| 733 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
| 734 |
]
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
[[package]]
|
| 737 |
name = "pydantic"
|
| 738 |
version = "2.13.4"
|
|
@@ -841,6 +867,22 @@ wheels = [
|
|
| 841 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/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" },
|
| 842 |
]
|
| 843 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
[[package]]
|
| 845 |
name = "python-dateutil"
|
| 846 |
version = "2.9.0.post0"
|
|
|
|
| 378 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
| 379 |
]
|
| 380 |
|
| 381 |
+
[[package]]
|
| 382 |
+
name = "iniconfig"
|
| 383 |
+
version = "2.3.0"
|
| 384 |
+
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 385 |
+
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
| 386 |
+
wheels = [
|
| 387 |
+
{ url = "https://pypi.tuna.tsinghua.edu.cn/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" },
|
| 388 |
+
]
|
| 389 |
+
|
| 390 |
[[package]]
|
| 391 |
name = "jinja2"
|
| 392 |
version = "3.1.6"
|
|
|
|
| 553 |
{ name = "httpx" },
|
| 554 |
]
|
| 555 |
|
| 556 |
+
[package.dev-dependencies]
|
| 557 |
+
dev = [
|
| 558 |
+
{ name = "pytest" },
|
| 559 |
+
]
|
| 560 |
+
|
| 561 |
[package.metadata]
|
| 562 |
requires-dist = [
|
| 563 |
{ name = "gradio", specifier = ">=6.18.0" },
|
| 564 |
{ name = "httpx", specifier = ">=0.28.1" },
|
| 565 |
]
|
| 566 |
|
| 567 |
+
[package.metadata.requires-dev]
|
| 568 |
+
dev = [{ name = "pytest", specifier = ">=9.1.0" }]
|
| 569 |
+
|
| 570 |
[[package]]
|
| 571 |
name = "orjson"
|
| 572 |
version = "3.11.9"
|
|
|
|
| 750 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
| 751 |
]
|
| 752 |
|
| 753 |
+
[[package]]
|
| 754 |
+
name = "pluggy"
|
| 755 |
+
version = "1.6.0"
|
| 756 |
+
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 757 |
+
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
| 758 |
+
wheels = [
|
| 759 |
+
{ url = "https://pypi.tuna.tsinghua.edu.cn/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" },
|
| 760 |
+
]
|
| 761 |
+
|
| 762 |
[[package]]
|
| 763 |
name = "pydantic"
|
| 764 |
version = "2.13.4"
|
|
|
|
| 867 |
{ url = "https://pypi.tuna.tsinghua.edu.cn/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" },
|
| 868 |
]
|
| 869 |
|
| 870 |
+
[[package]]
|
| 871 |
+
name = "pytest"
|
| 872 |
+
version = "9.1.0"
|
| 873 |
+
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 874 |
+
dependencies = [
|
| 875 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 876 |
+
{ name = "iniconfig" },
|
| 877 |
+
{ name = "packaging" },
|
| 878 |
+
{ name = "pluggy" },
|
| 879 |
+
{ name = "pygments" },
|
| 880 |
+
]
|
| 881 |
+
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
|
| 882 |
+
wheels = [
|
| 883 |
+
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
|
| 884 |
+
]
|
| 885 |
+
|
| 886 |
[[package]]
|
| 887 |
name = "python-dateutil"
|
| 888 |
version = "2.9.0.post0"
|