chore(merge): integrate workspace and frontend prototype

合并审阅 note(2026-10-05)

合并范围
- chore/workspace-bootstrap:b5b286c44f6a1456bef1db4ab904cecb92ff15a3。
- feat/frontend-pages:f108e88c47e6d604184d8a34dfd39be06762e159。
- 前端分支已包含工程骨架提交;一次合并保留两个分支的原有历史。

审阅结论
- 工程骨架与前端原型可以合入 main。
- 平台后端按业务域、Runtime、基础设施和后台进程划分;Rust 判题工程按契约、协调器、Worker、引擎和沙箱划分。
- 前端包含练习、比赛、训练、提交、账号和社区页面原型,以及独立入口的管理端;预览数据与未实现能力在界面中明确标记。
- 已修复编辑后立即刷新导致最后一段草稿丢失的问题(f108e88):页面隐藏、离开和 Vue scope 销毁时保存待写入内容,同时保留输入防抖和存储异常处理。

验证结果
- 本次完整 make check 通过:架构边界、Python/Rust 契约、Ruff、Rust fmt/clippy、前端 OpenAPI 类型、ESLint、Prettier、vue-tsc、生产构建及 Docker Compose 配置。
- Python 测试 7 项、Rust 集成测试 2 项、前端草稿回归测试 7 项全部通过。
- 前序浏览器验证覆盖桌面及移动布局、独立管理端入口、真实健康接口、编辑器 Worker;修复后已验证立即刷新可恢复最新草稿。

后续工作
- 编辑器页面生产 JS chunk 约 3,005.41 kB(gzip 785.90 kB),构建仍有包体告警,后续优化拆包和加载。
- 业务写入、认证和实际判题能力仍处于工程骨架阶段;沙箱继续拒绝未经验证隔离的执行。
This commit is contained in:
2026-10-05 18:29:40 +08:00
Notes: Kronecker 2026-10-05 18:29:40 +08:00
合并审阅 note(2026-10-05)

合并范围
- chore/workspace-bootstrap:b5b286c44f6a1456bef1db4ab904cecb92ff15a3。
- feat/frontend-pages:f108e88c47e6d604184d8a34dfd39be06762e159。
- 前端分支已包含工程骨架提交;一次合并保留两个分支的原有历史。

审阅结论
- 工程骨架与前端原型可以合入 main。
- 平台后端按业务域、Runtime、基础设施和后台进程划分;Rust 判题工程按契约、协调器、Worker、引擎和沙箱划分。
- 前端包含练习、比赛、训练、提交、账号和社区页面原型,以及独立入口的管理端;预览数据与未实现能力在界面中明确标记。
- 已修复编辑后立即刷新导致最后一段草稿丢失的问题(f108e88):页面隐藏、离开和 Vue scope 销毁时保存待写入内容,同时保留输入防抖和存储异常处理。

验证结果
- 本次完整 make check 通过:架构边界、Python/Rust 契约、Ruff、Rust fmt/clippy、前端 OpenAPI 类型、ESLint、Prettier、vue-tsc、生产构建及 Docker Compose 配置。
- Python 测试 7 项、Rust 集成测试 2 项、前端草稿回归测试 7 项全部通过。
- 前序浏览器验证覆盖桌面及移动布局、独立管理端入口、真实健康接口、编辑器 Worker;修复后已验证立即刷新可恢复最新草稿。

后续工作
- 编辑器页面生产 JS chunk 约 3,005.41 kB(gzip 785.90 kB),构建仍有包体告警,后续优化拆包和加载。
- 业务写入、认证和实际判题能力仍处于工程骨架阶段;沙箱继续拒绝未经验证隔离的执行。
157 changed files with 11968 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.{py,rs,toml}]
indent_size = 4
[Makefile]
indent_style = tab
+4
View File
@@ -0,0 +1,4 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.zip binary
+35
View File
@@ -0,0 +1,35 @@
# In-progress planning and engineering documents stay local; never publish to Gitea.
/docs
# Local configuration and credentials
.env
.env.*
!.env.example
!*.env.example
*.pem
*.key
# Tool caches and generated build output
.cache/
.tools/
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
node_modules/
dist/
target/
*.egg-info/
# Local data and editor state
.Trash-*/
.idea/
.vscode/
.DS_Store
*.log
deploy/data/
experiments/results/
!experiments/results/.gitkeep
+22
View File
@@ -0,0 +1,22 @@
# KF-OnlineJudge 工程约定
按现有模块边界扩展。本地存在 `docs/ENGINEERING.md` 和 `docs/DEVELOPMENT.md` 时先阅读;开发文档不在远端仓库中。
- 本机 `docs` 使用共享目录链接,Windows 和 CachyOS 使用同一份共享文档;实际目标由 `readlink docs` 确认。不创建另一份同步副本,不把源码、依赖或构建产物写回共享目录。
- `docs/` 中的开发文档仅保留本地,不加入 Git 提交,不推送到 Gitea;不得用 `git add -f` 绕过忽略规则。
- 分支采用 `feat/`、`fix/`、`chore/`、`perf/`、`test/` 等工程前缀,提交采用 Conventional Commits;不添加工具名称前缀或工具署名。
- 本仓库提交身份使用 `kronecker <2026692687@qq.com>`。
- 后端固定 Python 3.13.16,由 uv 管理;不要改用系统 Python 3.14 或用 uvx 运行项目服务。
- 在本机原生文件系统中开发,使用仓库根目录 Makefile;Python 环境为 `backend/.venv`,Rust 构建产物为 `judge/target`。
- 平台为模块化单体;业务域之间通过 `public.py` 调用,不直接操作其他模块的 ORM/repository。
- Runtime 通过注入端口调用业务工具;不导入业务模块或具体基础设施实现。
- Rust crate 依赖方向与 Python 分层由 `scripts/check_architecture.py` 检查。新增边界须同步更新架构说明与检查规则。
- 跨服务协议变更先更新模型和共享样例,运行 `make contracts`,保留 Python/Rust 的兼容验证。
- 数据库迁移只由后端 Alembic 管理;协调器控制执行状态,判题 Worker 不直接写评测数据库。
- 沙箱未验证隔离前拒绝执行;不得用直接运行宿主命令作为临时判题实现。
- 只建立当期需要的接口和实现;未实现能力明确报告,不提供模拟成绩或虚假任务成功状态。
- 使用项目锁文件;新增依赖说明职责。凭据、私有代码和隐藏测试数据不进入仓库。
按变更运行 `make check-backend`、`make check-judge`、`make check-frontend` 和 `make architecture`。
跨工程骨架或契约改动运行完整 `make check`,记录实际通过项和未验证的能力。
沙箱的权限、隔离、超限和破坏性测试单独在规定执行环境验证。
+83
View File
@@ -0,0 +1,83 @@
SHELL := /bin/bash
CARGO ?= $(if $(wildcard $(HOME)/.cargo/bin/cargo),$(HOME)/.cargo/bin/cargo,cargo)
UV ?= uv
PNPM ?= pnpm
.PHONY: help install check architecture check-backend check-frontend check-judge contracts format api frontend coordinator sandbox-probe worker-check infra-config infra-up infra-down
help:
@echo 'install 安装锁定依赖并编译 Rust workspace'
@echo 'check 架构、契约、静态检查、测试与前端构建'
@echo 'contracts 更新 JSON Schema、OpenAPI 与前端类型'
@echo 'format 格式化 Python、Rust 与前端源码'
@echo 'api / frontend 启动平台 API / 前端开发服务'
@echo 'coordinator 启动协调器 HTTP 骨架'
@echo 'sandbox-probe 只读探测当前 Linux 环境'
@echo 'worker-check 验证三个后台进程入口,不消费任务'
@echo 'infra-config 检查本地 PostgreSQL / RabbitMQ 配置'
@echo 'infra-up/down 启动/停止本地开发依赖(保留数据卷)'
install:
cd backend && $(UV) sync --locked
cd frontend && $(PNPM) install --frozen-lockfile
cd judge && $(CARGO) build --workspace --locked
check: architecture check-backend check-judge check-frontend infra-config
architecture:
python3 scripts/check_architecture.py
check-backend:
cd backend && $(UV) run --locked ruff check . ../scripts
cd backend && $(UV) run --locked ruff format --check . ../scripts
cd backend && $(UV) run --locked pytest
cd backend && $(UV) run --locked python -m kf_oj.contracts.export --check
check-judge:
cd judge && $(CARGO) fmt --all -- --check
cd judge && $(CARGO) clippy --workspace --all-targets --locked -- -D warnings
cd judge && $(CARGO) test --workspace --locked
check-frontend:
cd frontend && $(PNPM) check:api
cd frontend && $(PNPM) lint
cd frontend && $(PNPM) format:check
cd frontend && $(PNPM) typecheck
cd frontend && $(PNPM) test
cd frontend && $(PNPM) build
contracts:
cd backend && $(UV) run --locked python -m kf_oj.contracts.export
cd frontend && $(PNPM) generate:api
format:
cd backend && $(UV) run --locked ruff check --fix . ../scripts
cd backend && $(UV) run --locked ruff format . ../scripts
cd judge && $(CARGO) fmt --all
cd frontend && $(PNPM) format
api:
cd backend && $(UV) run --locked uvicorn kf_oj.api.app:create_app --factory --reload --host 127.0.0.1 --port 8000
frontend:
cd frontend && $(PNPM) dev
coordinator:
cd judge && $(CARGO) run --locked -p kf-coordinator
sandbox-probe:
cd judge && $(CARGO) run --locked -p kf-sandbox -- probe
worker-check:
cd backend && $(UV) run --locked kf-learning-worker --check
cd backend && $(UV) run --locked kf-outbox-publisher --check
cd judge && $(CARGO) run --locked -p kf-worker -- --check
infra-config:
docker compose --env-file deploy/.env.example -f deploy/compose.dev.yml config --quiet
infra-up:
docker compose --env-file deploy/.env -f deploy/compose.dev.yml up -d --wait
infra-down:
docker compose --env-file deploy/.env -f deploy/compose.dev.yml down
+71
View File
@@ -1,2 +1,73 @@
# KF-OnlineJudge
以 Rust 自研判题与 Linux 沙箱、以 Python/FastAPI 提供平台服务的在线评测系统。
前端使用 Vue 3、TypeScript、Element Plus 和 Vite。
当前版本为 **0.0.1 工程骨架**:平台 API 和协调器提供存活接口,前端可检查实际 API 连接。
沙箱隔离执行、判题、账号、题库、提交与学习业务尚未实现。
## 工程划分
| 目录 | 职责 |
| --- | --- |
| `backend/` | Python 模块化单体、平台 API、学习 Worker、待投递事件发布器及唯一数据库迁移入口 |
| `judge/` | Rust workspace:协议、沙箱、判题编排、判题 Worker 和协调器 |
| `frontend/` | 页面、路由、请求生命周期与生成的 API 类型 |
| `contracts/` | 版本化 JSON Schema 与 OpenAPI 快照 |
| `deploy/` | 本地 PostgreSQL / RabbitMQ 配置 |
| `scripts/` | Python 导入边界与 Rust crate 依赖检查 |
| `tests/` | 跨语言共享协议样例 |
| `skills/`、`experiments/` | 后续内置技能与可复现实验的扩展位置 |
Python 业务域分为 `identity`、`problems`、`submissions`、`community`、`learning`、`skills`
和 `operations`。业务域之间通过公开应用接口协作,不直接访问其他域的持久化实现。
`runtime` 管理执行机制,通过注入端口调用业务能力。
Rust 分为 `kf-contracts`、`kf-sandbox`、`kf-engine`、`kf-worker`、`kf-coordinator` 五个 crate。
协调器拥有执行状态写入权,判题 Worker 不直接写评测数据库。
沙箱尚未验证隔离能力,当前执行入口明确拒绝运行程序。
## 安装与检查
工具链基线:Python 3.13.16 + uv、Rust 1.99.0、Node 26.10.0 + pnpm 11.28.4。
Python 和 Rust 版本由工程配置固定,三套工程分别提交依赖锁文件。
开发与构建使用 Linux 原生文件系统。
```bash
make install
make check
```
`make check` 包含架构边界、契约同步、静态检查、测试、前端生产构建和 Compose 配置检查。
检查无需启动数据库或队列,也不执行破坏性沙箱测试。
`make format` 格式化源码,`make contracts` 更新 Schema、OpenAPI 和前端类型。
## 启动骨架
分别在终端运行:
```bash
make api # 平台 API:127.0.0.1:8000
make frontend # Vue 开发服务:127.0.0.1:5173
make coordinator # 协调器:127.0.0.1:8100
```
平台存活接口为 `/api/v1/health/live`,开发 API 文档为 `/api/docs`。
前端开发服务将 `/api` 代理到平台;生产部署需另行配置反向代理与 SPA 路由回退。
协调器存活接口为 `/health/live`。
```bash
make worker-check # 三个后台入口仅检查配置,不消费任务
make sandbox-probe # 只读观察 Linux 环境,不创建隔离资源
```
本地数据库与队列的启动步骤见 [部署说明](deploy/README.md)。
业务表和数据库 revision 尚未建立;迁移统一由后端 Alembic 管理。
模型服务与第三方插件尚未接入。
开发中的规划文档仅在本地维护,不包含在远端仓库中。
后续逐步实现沙箱、单机判题、分布式恢复、并行优化和学习功能。
## 许可证
[MIT](LICENSE)。引用或复用其他项目代码时记录来源并遵守对应许可证。
+5
View File
@@ -0,0 +1,5 @@
KF_OJ_ENVIRONMENT=development
KF_OJ_LOG_LEVEL=INFO
# Optional until database-backed use cases are implemented.
KF_OJ_DATABASE_URL=postgresql+asyncpg://kf_oj:local-dev-only@127.0.0.1:5432/kf_oj
KF_OJ_AMQP_URL=amqp://kf_oj:local-dev-only@127.0.0.1:5672/kf_oj
+1
View File
@@ -0,0 +1 @@
3.13.16
+25
View File
@@ -0,0 +1,25 @@
# Python 平台工程
固定 Python 3.13.16,使用 uv 管理项目与锁文件。API、待投递事件发布器和学习 Worker
共用一个 Python 包、一个依赖锁文件,分别运行独立进程。
业务模块位于 `src/kf_oj/modules/`;HTTP 在 `api/`;通用适配器在 `infra/`;
Agent 执行机制在 `runtime/`;进程组合入口在 `workers/`。
业务域之间通过公开应用接口调用,不直接访问其他域的 repository/models。
Runtime 通过注入端口调用业务能力;导入边界由根目录的架构检查脚本验证。
```bash
# 从仓库根目录调用。
make install
make api
make worker-check
```
API 当前只有 `/api/v1/health/live`,不连接数据库或消息队列。
Worker 目前仅支持 `--check`,默认拒绝进入尚未实现的任务循环。
日志、配置和迁移入口已建立;业务表、登录、判题投递和模型接入尚未实现。
Python 虚拟环境位于 `backend/.venv`,可在 `backend/` 直接使用 `uv sync --locked` 和 `uv run --locked`。
数据库由 Alembic 唯一管理;业务模型落地后在 `migrations/env.py` 中显式登记元数据。
不要用 API 启动时的 `create_all` 代替迁移,也不要让 Rust 维护另一套迁移历史。
配置示例在 `.env.example`;执行迁移前复制为 `.env` 并设置实际数据库连接。
+37
View File
@@ -0,0 +1,37 @@
[alembic]
script_location = %(here)s/migrations
prepend_sys_path = .
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+52
View File
@@ -0,0 +1,52 @@
import asyncio
from logging.config import fileConfig
from alembic import context
from sqlalchemy import Connection
from kf_oj.core.settings import Settings
from kf_oj.infra.db.base import Base
from kf_oj.infra.db.session import build_engine
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Register each business module's ORM models here when its schema is implemented.
target_metadata = Base.metadata
def run_offline() -> None:
settings = Settings()
if settings.database_url is None:
raise ValueError("KF_OJ_DATABASE_URL is required for migrations")
context.configure(
url=settings.database_url.get_secret_value(),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def migrate(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
async def run_online() -> None:
engine = build_engine(Settings())
try:
async with engine.connect() as connection:
await connection.run_sync(migrate)
finally:
await engine.dispose()
if context.is_offline_mode():
run_offline()
else:
asyncio.run(run_online())
+24
View File
@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: str | None = ${repr(down_revision)}
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
+46
View File
@@ -0,0 +1,46 @@
[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"
[project]
name = "kf-oj-platform"
version = "0.0.1"
description = "KF-OnlineJudge platform and learning workers"
readme = "README.md"
requires-python = ">=3.13,<3.14"
dependencies = [
"fastapi>=0.115,<1",
"pydantic>=2.10,<3",
"pydantic-settings>=2.7,<3",
"uvicorn>=0.34,<1",
"sqlalchemy[asyncio]>=2.0,<3",
"asyncpg>=0.30,<1",
"alembic>=1.15,<2",
"aio-pika>=9.5,<10",
]
[project.scripts]
kf-learning-worker = "kf_oj.workers.learning:main"
kf-outbox-publisher = "kf_oj.workers.outbox:main"
[dependency-groups]
dev = [
"ruff>=0.11,<1",
"pytest>=8.3,<10",
"httpx>=0.28,<1",
"jsonschema>=4.23,<5",
]
[tool.hatch.build.targets.wheel]
packages = ["src/kf_oj"]
[tool.ruff]
target-version = "py313"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"
+3
View File
@@ -0,0 +1,3 @@
"""KF-OnlineJudge platform, application modules and learning workers."""
__version__ = "0.0.1"
+1
View File
@@ -0,0 +1 @@
"""HTTP transport and application composition."""
+36
View File
@@ -0,0 +1,36 @@
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from kf_oj import __version__
from kf_oj.api.v1.router import router
from kf_oj.core.logging import configure_logging
from kf_oj.core.settings import Settings
def create_app(settings: Settings | None = None) -> FastAPI:
settings = settings or Settings()
@asynccontextmanager
async def lifespan(_: FastAPI):
configure_logging(settings.log_level)
logger = logging.getLogger("kf_oj.api")
logger.info("Platform API started")
try:
yield
finally:
logger.info("Platform API stopped")
app = FastAPI(
title="KF-OnlineJudge Platform API",
version=__version__,
description="工程骨架。当前只提供进程存活检查,业务接口逐项实现。",
docs_url="/api/docs" if settings.environment != "production" else None,
redoc_url=None,
openapi_url="/api/openapi.json" if settings.environment != "production" else None,
lifespan=lifespan,
)
app.state.settings = settings
app.include_router(router, prefix="/api/v1")
return app
+1
View File
@@ -0,0 +1 @@
"""Platform API version 1."""
+12
View File
@@ -0,0 +1,12 @@
from fastapi import APIRouter
from kf_oj import __version__
from kf_oj.contracts.health import HealthResponse
router = APIRouter()
@router.get("/health/live", response_model=HealthResponse, tags=["health"], operation_id="liveness")
async def liveness() -> HealthResponse:
"""Check process liveness only; does not attest DB, queue or judge readiness."""
return HealthResponse(status="ok", service="platform", version=__version__)
+1
View File
@@ -0,0 +1 @@
"""Versioned transport models; never import business or infrastructure code."""
+41
View File
@@ -0,0 +1,41 @@
"""Export deterministic public contracts without opening DB or queue connections."""
import argparse
import json
from pathlib import Path
from kf_oj.api.app import create_app
from kf_oj.contracts.judge import JudgeTaskNotification
from kf_oj.core.settings import Settings
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Fail if checked-in files are stale")
args = parser.parse_args()
root = Path(__file__).resolve().parents[4]
schema = JudgeTaskNotification.model_json_schema()
schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
schema["$id"] = "urn:kf-oj:judge-task:v1"
outputs = {
root / "contracts/schemas/judge-task.v1.schema.json": schema,
root / "contracts/openapi/platform.v1.json": create_app(
Settings(_env_file=None, environment="test")
).openapi(),
}
stale = []
for path, document in outputs.items():
content = json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
if args.check:
if not path.exists() or path.read_text(encoding="utf-8") != content:
stale.append(str(path.relative_to(root)))
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
print(f"Exported {path.relative_to(root)}")
if stale:
parser.exit(1, "Stale contracts: " + ", ".join(stale) + "; run make contracts\n")
if __name__ == "__main__":
main()
+11
View File
@@ -0,0 +1,11 @@
from typing import Literal
from pydantic import BaseModel, ConfigDict
class HealthResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
status: Literal["ok"]
service: Literal["platform"]
version: str
+14
View File
@@ -0,0 +1,14 @@
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict
class JudgeTaskNotification(BaseModel):
"""A wake-up notification, not execution authority or the source of task state."""
model_config = ConfigDict(extra="forbid", frozen=True)
schema_version: Literal[1]
kind: Literal["judge.evaluation"]
evaluation_id: UUID
+1
View File
@@ -0,0 +1 @@
"""Cross-cutting configuration and logging; no business or transport dependencies."""
+22
View File
@@ -0,0 +1,22 @@
import json
import logging
from datetime import UTC, datetime
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"timestamp": datetime.fromtimestamp(record.created, UTC).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
def configure_logging(level: str) -> None:
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=level, handlers=[handler], force=True)
+13
View File
@@ -0,0 +1,13 @@
from typing import Literal
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="KF_OJ_", env_file=".env", extra="ignore")
environment: Literal["development", "test", "production"] = "development"
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
database_url: SecretStr | None = None
amqp_url: SecretStr | None = None
+1
View File
@@ -0,0 +1 @@
"""Shared database, messaging and artifact adapters; no HTTP or business orchestration."""
@@ -0,0 +1 @@
"""Reserved for authenticated artifact access and digest verification."""
+1
View File
@@ -0,0 +1 @@
"""Database primitives; all schema migrations live in backend/migrations."""
+14
View File
@@ -0,0 +1,14 @@
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
metadata = MetaData(
naming_convention={
"ix": "ix_%(table_name)s_%(column_0_name)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
)
+13
View File
@@ -0,0 +1,13 @@
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from kf_oj.core.settings import Settings
def build_engine(settings: Settings) -> AsyncEngine:
if settings.database_url is None:
raise ValueError("KF_OJ_DATABASE_URL is required for database-backed operations")
return create_async_engine(settings.database_url.get_secret_value(), pool_pre_ping=True)
def build_session_factory(engine: AsyncEngine) -> async_sessionmaker:
return async_sessionmaker(engine, expire_on_commit=False)
@@ -0,0 +1 @@
"""Reserved for AMQP publishing/consuming adapters, confirms and retry transport."""
+1
View File
@@ -0,0 +1 @@
"""Business modules; cross-module calls go through explicit public application interfaces."""
@@ -0,0 +1,6 @@
# community
负责题目讨论、题解、评论、作者归属、可见性及内容修改记录。
题目和账号信息通过对应模块的公开接口查询,不直接操作其 ORM 表。
技能社区的技能版本与发布语义归 `skills` 管理,本模块只提供可复用的内容/反馈能力。
当前未实现社区接口。
@@ -0,0 +1 @@
"""Problem discussions, solutions, comments and content visibility (PF-04)."""
@@ -0,0 +1,5 @@
# identity
负责账号、登录凭证、会话与角色;向其他模块提供经过验证的用户身份和授权依据。
后续 `public.py` 暴露身份查询接口,`service.py` 编排用例,`repository.py` 封装持久化。
用户身份不能来自前端自报字段或模型参数。目前仅建立模块边界,未实现登录。
@@ -0,0 +1 @@
"""Accounts, credentials, roles and trusted user identity (PF-02)."""
@@ -0,0 +1,7 @@
# learning
负责学习证据、统计与代码差异、题库筛选、分层答疑、计划校验和反馈。
读取其他业务域的授权公开接口,通过组合层向 `runtime` 注入受控业务工具。
规则推荐属于业务模块;步骤执行、预算与检查点属于 `runtime`。
基础设施重试不能算成学生失败;模型输出必须校验后才能保存业务结果。
当前未接入模型,也未创建训练计划表。
@@ -0,0 +1 @@
"""Evidence, recommendations, tutoring, training plans and feedback (AI-01..05)."""
@@ -0,0 +1,5 @@
# operations
负责平台侧节点、任务积压、重试原因和审计查询的管理视图。
对判题状态的操作必须调用协调器协议;观测模块不成为第二个状态写入入口。
当前只有目录边界,节点登记、指标聚合和管理员接口待实现。
@@ -0,0 +1 @@
"""Read-only node/task observability and administrator views (PF-05)."""
@@ -0,0 +1,5 @@
# problems
负责题目逻辑身份、不可变版本、导入、标签、限制、发布状态与测试工件元数据。
跨模块访问通过后续 `public.py`;大文件存取通过 `infra/artifacts` 的适配器。
隐藏数据与公开样例分别授权。当前未创建题库表或导入接口。
@@ -0,0 +1 @@
"""Problem catalog, immutable versions, import and publication (PF-02)."""
@@ -0,0 +1,7 @@
# skills
负责声明式技能包、不可变版本、个人绑定和参数、启停及后续分享生命周期。
包内容与用户私有学习档案分开存储;工具声明不能增加后端授权。
`runtime` 只接收已经校验的固定版本和参数,不导入本模块实现。
仓库根目录 `skills/` 保存未来平台维护的静态内置内容;用户绑定属于数据库业务记录。
当前未建立包解析器,不执行任何插件脚本。
@@ -0,0 +1 @@
"""Skill definitions, immutable versions, user bindings and publication (SP-01..04)."""
@@ -0,0 +1,6 @@
# submissions
负责提交幂等请求、不可变源码引用、创建初始评测、历史查询与学生可见结果。
创建提交、评测和待投递事件必须处于同一事务。
协调器拥有领取后的执行状态与最终结果写入权;本模块不能绕过协议覆盖终态。
后续 `public.py` 提供授权后的学习证据入口。当前未实现提交接口。
@@ -0,0 +1 @@
"""Source snapshots, submission creation, evaluation references and queries (PF-03)."""
+5
View File
@@ -0,0 +1,5 @@
"""Reserved for bounded Agent execution, tool ports, budgets and checkpoints.
Application tools are injected by the learning composition layer. This package
does not import business modules, database adapters or any model SDK.
"""
+1
View File
@@ -0,0 +1 @@
"""Independent process composition roots; task loops are implemented per milestone."""
+25
View File
@@ -0,0 +1,25 @@
import argparse
import json
from kf_oj import __version__
from kf_oj.core.settings import Settings
def check_entrypoint(service: str) -> None:
parser = argparse.ArgumentParser(description=f"{service} process entrypoint")
parser.add_argument("--check", action="store_true", help="Validate configuration and exit")
args = parser.parse_args()
settings = Settings()
if not args.check:
parser.exit(2, f"{service}: task loop is not implemented; use --check\n")
print(
json.dumps(
{
"service": service,
"version": __version__,
"environment": settings.environment,
"status": "scaffold",
"consuming_tasks": False,
}
)
)
+9
View File
@@ -0,0 +1,9 @@
from kf_oj.workers.bootstrap import check_entrypoint
def main() -> None:
check_entrypoint("learning-worker")
if __name__ == "__main__":
main()
+9
View File
@@ -0,0 +1,9 @@
from kf_oj.workers.bootstrap import check_entrypoint
def main() -> None:
check_entrypoint("outbox-publisher")
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
import json
from pathlib import Path
import pytest
from jsonschema import Draft202012Validator, FormatChecker
from pydantic import ValidationError
from kf_oj.contracts.judge import JudgeTaskNotification
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = ROOT / "tests/fixtures/contracts"
SCHEMA = json.loads((ROOT / "contracts/schemas/judge-task.v1.schema.json").read_text())
@pytest.mark.parametrize("path", sorted(FIXTURES.glob("judge-task.*.json")), ids=lambda p: p.stem)
def test_shared_wire_examples(path: Path) -> None:
payload = json.loads(path.read_text())
validator = Draft202012Validator(SCHEMA, format_checker=FormatChecker())
if ".invalid-" in path.name:
with pytest.raises(ValidationError):
JudgeTaskNotification.model_validate(payload)
assert not validator.is_valid(payload)
else:
notification = JudgeTaskNotification.model_validate(payload)
validator.validate(payload)
assert notification.model_dump(mode="json") == payload
+33
View File
@@ -0,0 +1,33 @@
import asyncio
import httpx
from kf_oj.api.app import create_app
from kf_oj.core.settings import Settings
def test_liveness_without_database_or_queue() -> None:
settings = Settings(_env_file=None, environment="test", database_url=None, amqp_url=None)
async def request():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_app(settings)), base_url="http://test"
) as client:
return await client.get("/api/v1/health/live")
response = asyncio.run(request())
assert response.status_code == 200
assert response.json() == {"status": "ok", "service": "platform", "version": "0.0.1"}
def test_production_disables_api_documentation() -> None:
app = create_app(Settings(_env_file=None, environment="production"))
async def request():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://test"
) as client:
for path in ("/api/docs", "/api/openapi.json"):
assert (await client.get(path)).status_code == 404
asyncio.run(request())
+677
View File
@@ -0,0 +1,677 @@
version = 1
revision = 5
requires-python = "==3.13.*"
[[package]]
name = "aio-pika"
version = "9.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiormq" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" },
]
[[package]]
name = "aiormq"
version = "6.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pamqp" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" },
]
[[package]]
name = "alembic"
version = "1.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]]
name = "annotated-types"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
]
[[package]]
name = "anyio"
version = "4.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
[[package]]
name = "asyncpg"
version = "0.31.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
]
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "click"
version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[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 = "fastapi"
version = "0.142.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/4f/f7c30a73127e0a8bbffe788369b8359e530b01ae06e2757936fa35bc5e6d/fastapi-0.142.2.tar.gz", hash = "sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570", size = 466721, upload-time = "2026-09-30T09:51:11.277Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/b6/78aaf9141fb46742928c113f3cf6ef2259d538cb02b604b7656c1dc9883c/fastapi-0.142.2-py3-none-any.whl", hash = "sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b", size = 144409, upload-time = "2026-09-30T09:51:09.8Z" },
]
[[package]]
name = "greenlet"
version = "3.5.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" },
{ url = "https://files.pythonhosted.org/packages/c6/7c/40400455f5b5a65bb83e94fde66d1be9e5ec518638113f8083ace746c309/greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e", size = 632632, upload-time = "2026-09-14T15:25:07.813Z" },
{ url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" },
{ url = "https://files.pythonhosted.org/packages/f9/67/1f35cff30a6c51c3f23b63d4afcc7313ab4f97490ba3676fa78178984b27/greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e", size = 434933, upload-time = "2026-09-14T15:28:38.858Z" },
{ url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" },
{ url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" },
{ url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" },
{ url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" },
]
[[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.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
]
[[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 = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
name = "kf-oj-platform"
version = "0.0.1"
source = { editable = "." }
dependencies = [
{ name = "aio-pika" },
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "sqlalchemy", extra = ["asyncio"] },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
{ name = "jsonschema" },
{ name = "pytest" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "aio-pika", specifier = ">=9.5,<10" },
{ name = "alembic", specifier = ">=1.15,<2" },
{ name = "asyncpg", specifier = ">=0.30,<1" },
{ name = "fastapi", specifier = ">=0.115,<1" },
{ name = "pydantic", specifier = ">=2.10,<3" },
{ name = "pydantic-settings", specifier = ">=2.7,<3" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0,<3" },
{ name = "uvicorn", specifier = ">=0.34,<1" },
]
[package.metadata.requires-dev]
dev = [
{ name = "httpx", specifier = ">=0.28,<1" },
{ name = "jsonschema", specifier = ">=4.23,<5" },
{ name = "pytest", specifier = ">=8.3,<10" },
{ name = "ruff", specifier = ">=0.11,<1" },
]
[[package]]
name = "mako"
version = "1.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/09/e07c4b5579a79f4b16f8d4f29f6c54514ac787c4ad506b8c4f28a0e6b0bf/mako-1.4.3.tar.gz", hash = "sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a", size = 412799, upload-time = "2026-09-22T20:54:31.509Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/a0/053d6af3e8f871e0073b4a36732d9e65be77a72e5434c31b94f6af78a6bb/mako-1.4.3-py3-none-any.whl", hash = "sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f", size = 80164, upload-time = "2026-09-22T20:54:33.128Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" },
{ url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" },
{ url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" },
{ url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" },
{ url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" },
{ url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" },
{ url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" },
{ url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" },
{ url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" },
{ url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" },
{ url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" },
{ url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" },
{ url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" },
{ url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" },
{ url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" },
{ url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" },
{ url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" },
]
[[package]]
name = "multidict"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/64/642465a4827331a98ba4ae29f97658be25d1bdcb868872a2f78f7482b507/multidict-7.0.0.tar.gz", hash = "sha256:a7fcd089a0af2e0ef053c0d39c22c9ebf2434dddcb91034fd2f59ec99623788e", size = 280561, upload-time = "2026-09-26T19:36:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/6e/c388fccb88dd6cb41ce2454e420c869fb0629ce10bfe12e4cad578ba940d/multidict-7.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:15d9ad70615ce1b37c124445145f071ba4eaa88cb21b49a6b81727cc85aefc37", size = 92310, upload-time = "2026-09-26T19:32:24.366Z" },
{ url = "https://files.pythonhosted.org/packages/58/2f/33505a5c498613d53ab1d87b3c9425f6757afc7e48c2409f7aa001d0cdc1/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:313e336c75a53447cd943d93235c4702428b9740cb681e7174e398ba1f3f2918", size = 85114, upload-time = "2026-09-26T19:32:25.975Z" },
{ url = "https://files.pythonhosted.org/packages/76/f5/27b19597821cb80d082b649e5e6e01033aed5e0eebdbbc6a644eb49515a7/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a4838959217e2652ef7dce1d01bb370aed3e5b694eb7a8258fa1cc50beef08ca", size = 86952, upload-time = "2026-09-26T19:32:27.429Z" },
{ url = "https://files.pythonhosted.org/packages/f7/58/a5e44d5453c8bb295979d242cfd3beee06f6ab9f6c1c06a13c2191b792b1/multidict-7.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd6520538e19fe104c3b74cc64767d6f8959dd2c9b8d59da62d83641ea646deb", size = 147356, upload-time = "2026-09-26T19:32:28.816Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c0/4da412a7267ac85759300a078f3bdad986e18a30c1ead8b74d2e3b335a2b/multidict-7.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e9bf055becb08ff797d5cf80b760b726b8babb39046564d2b8481bbe6504bce", size = 88022, upload-time = "2026-09-26T19:32:30.44Z" },
{ url = "https://files.pythonhosted.org/packages/b6/46/bebeb7df10298d390942e61bc7fc48472183ee4f5a0b7c285d93369b6874/multidict-7.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8e443cab381f8b6d24950982ae508600d464412cbdcf78e263cf8cd9b8372d68", size = 87046, upload-time = "2026-09-26T19:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/ab/d3/38b254c21cb8e9a65f8751ad8ba0a6f9baad75e3b6d3e559d1d05db26482/multidict-7.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e45bb7b9c850f216c0081dab8c6862fdec6b8be1c800b2a1aa4d1e56dcfacb54", size = 488729, upload-time = "2026-09-26T19:32:33.338Z" },
{ url = "https://files.pythonhosted.org/packages/2a/28/19d66c476cc44fffdeacd5227a59046f94612075008069286bdbb9d3fa35/multidict-7.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2f742000b47d806e5798f5ef0cdb2ed43d822fc913331a33ed1a40515495a7", size = 496708, upload-time = "2026-09-26T19:32:35.286Z" },
{ url = "https://files.pythonhosted.org/packages/0d/66/3bb2d2cdf67742b002985906785b0db1584ca2e67ddb3677f6e9614321d6/multidict-7.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70e28954ae5ae3c1806aa6d85bbb26ef7ab73017228eb236a3f245e24720bb34", size = 458261, upload-time = "2026-09-26T19:32:37.027Z" },
{ url = "https://files.pythonhosted.org/packages/af/5c/c8ca15c191f957e8d86ec65148ed1e8ca189f456e66153e23c4ae610649a/multidict-7.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c41ccfa34b152146ccf05e8da753a85f8e67437477f8a00783aa427a132abd31", size = 511932, upload-time = "2026-09-26T19:32:39.025Z" },
{ url = "https://files.pythonhosted.org/packages/14/ff/0665b2c3d5e42165a96fffc4c34c3d5fa66d0819d82b9bc2977b9a6aa1ed/multidict-7.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a1de5f1da3ae056b45b6599a53dba6c86879bdfa5c3f15d7dde99f6cfbc03c6", size = 529152, upload-time = "2026-09-26T19:32:41.068Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f7/967875e5d1f908613cfcd5d9d078011168acb88a128028b81a0de43e8aa6/multidict-7.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:094852ebc620a10ee0e11f36a68e016d73596c0f5ed8351a9a52fcff7d449ad6", size = 506337, upload-time = "2026-09-26T19:32:42.839Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f9/56f04cf560bf736c23ba9cacc1ae3fcd2bbb591ba4f166c4bc08f5fead61/multidict-7.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03cc4027103bb61feac20336a15fced1a6a369fed6a61c516353921207e4578c", size = 448144, upload-time = "2026-09-26T19:32:44.556Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/ff7978bb38f2e2908cf7fc80061175cbfacdcada248dc48fb79a10c0e92b/multidict-7.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0fef8622896443fd0bdf39586391c2162ac05004ba4c9918827dfd2f79da750c", size = 485535, upload-time = "2026-09-26T19:32:46.439Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6c/1e9a6457587da0a31b3ef31ca0a40624877ca29408c8a7385260a3d45cd9/multidict-7.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1847d33ca198aba3bf5fad036a7e0376cca325e7ec89f26362f9aa48994d8146", size = 471078, upload-time = "2026-09-26T19:32:48.166Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/ee11505f0ae23a344544010485db982290c0e6105caefd74ce3830c315ca/multidict-7.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b5c93f582f8a973ea803c46097729e34969683bc99f99fc00321b1f76317e81", size = 490764, upload-time = "2026-09-26T19:32:49.861Z" },
{ url = "https://files.pythonhosted.org/packages/b9/8d/1e954f7cbb1f2080c2712b6a4cb6ce196037c06c686ae030003465b65ad7/multidict-7.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7c717376f801dd1ee102b292e0a4ca6037e948a8710316e64e698fe39eeb1307", size = 500752, upload-time = "2026-09-26T19:32:51.756Z" },
{ url = "https://files.pythonhosted.org/packages/da/54/0f3899f51364f502219192b8c03da123c8618e9709499194ecd8e4143cc9/multidict-7.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c6d6e73b9f7b70558de5624b036815464a7ead4671fe2c3a4b3ce444c240cba6", size = 444706, upload-time = "2026-09-26T19:32:53.515Z" },
{ url = "https://files.pythonhosted.org/packages/54/03/8edc3cb05a35185a4910582a50aa4dd342cceda3b26b2053dbd004adc9e8/multidict-7.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:18f627c69d041ae17d8fb45dbc63e192a2afc3ba062195441b5a50e93c50ae68", size = 511149, upload-time = "2026-09-26T19:32:55.281Z" },
{ url = "https://files.pythonhosted.org/packages/d1/b4/4234b76f97b2966d1de2f3507f5dc9af4072e2ed16cfb476855490e34ae9/multidict-7.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8477e14353097077e1011464794d11e1d7559e821ab7c612cf7c06e7826ee1ba", size = 501451, upload-time = "2026-09-26T19:32:57.073Z" },
{ url = "https://files.pythonhosted.org/packages/d8/a9/fe7965b74cb3cf822d3e88063f13e3eff2600c36022f69c603b5b52f3e21/multidict-7.0.0-cp313-cp313-win32.whl", hash = "sha256:2efca6de4fbefc12f2aaac2d5e1c176a3864d22250246b44fe3823e3634f06f8", size = 76414, upload-time = "2026-09-26T19:32:58.84Z" },
{ url = "https://files.pythonhosted.org/packages/e9/c7/a4960e81ba8a3a9698011d1ac92d4d11ce8c9bb0ed6d235de76c49cae3d1/multidict-7.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:188a7f89a1935a138237088bff22400bacd25d80fb0d3a8a26fab25961df2239", size = 87005, upload-time = "2026-09-26T19:33:00.576Z" },
{ url = "https://files.pythonhosted.org/packages/6c/66/7f6b0898c895f2b39ebaa3fc34d32537d263aa5c74809ee747b5e705da80/multidict-7.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:debb701a0436c96f2717c6156428a2c8bce2559236e120a674d503aeed678aa3", size = 85466, upload-time = "2026-09-26T19:33:02.072Z" },
{ url = "https://files.pythonhosted.org/packages/46/a4/a3e9e72ed0f99587e9e96c3de72b743e1385e5e2b15fb583d8dfc05ffffb/multidict-7.0.0-py3-none-any.whl", hash = "sha256:1ba6a45450243bd8953223adff7183fce5e3f9f3556a03bd17a674caaa046215", size = 26592, upload-time = "2026-09-26T19:36:09.764Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "pamqp"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" },
]
[[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 = "propcache"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" },
{ url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" },
{ url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" },
{ url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" },
{ url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" },
{ url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" },
{ url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" },
{ url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" },
{ url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" },
{ url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" },
{ url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" },
{ url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" },
{ url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" },
{ url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" },
{ url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" },
]
[[package]]
name = "pydantic"
version = "2.13.5"
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/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
]
[[package]]
name = "pydantic-settings"
version = "2.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[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 = "python-dotenv"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "rpds-py"
version = "2026.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
{ url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
{ url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
{ url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
{ url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
{ url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
{ url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
{ url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
{ url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
{ url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
{ url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
{ url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
]
[[package]]
name = "ruff"
version = "0.16.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" },
{ url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" },
{ url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" },
{ url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" },
{ url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" },
{ url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" },
{ url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" },
{ url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" },
{ url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" },
{ url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" },
{ url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" },
{ url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
]
[[package]]
name = "sqlalchemy"
version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/4b/81d972a46c9f1d978af1795e2abc988855c711453552cb45d75f6e4abbf5/sqlalchemy-2.1.3.tar.gz", hash = "sha256:ade5281df06038c6394f532590d592d3381ee5d11b9e0006a2570aef41145118", size = 10525778, upload-time = "2026-10-02T22:57:00.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/71/68c29931fb2c5178241f7d0c63dbdd800ad65685eac70d7f2117145b5e3c/sqlalchemy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:260bdd8baa1dc631d76d14b26ad009ebbb665b4469666afef8dcaa1f964016cb", size = 2457479, upload-time = "2026-10-03T02:39:19.741Z" },
{ url = "https://files.pythonhosted.org/packages/94/21/5dbd91816b2cdb367bca3195f300ffeca916762e088d6924fc33a18f50ad/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fc01a8047d6a4f4e997441994f07af79fdd470fc80669751e32f91153d6b491", size = 4582199, upload-time = "2026-10-02T23:37:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/32/d1/79962ce06d6cd34516def113f5170184fa9401ebddc59fd6cb18f6e919f3/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b1b3ff10eba0f9290142375e667351c99d24ee7fcff6e0f85bd8be9d39b50a17", size = 4632923, upload-time = "2026-10-02T23:35:55.318Z" },
{ url = "https://files.pythonhosted.org/packages/3c/94/03e98031dc9a1738c580c3bf01beeafe695f247eb647f4e8dda3fab2454f/sqlalchemy-2.1.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:efb1ebf695e80d9a69f5bbff9d812d2491fd168b27d6b42ad4c8a4d3d6faaef1", size = 4300224, upload-time = "2026-10-02T23:46:45.495Z" },
{ url = "https://files.pythonhosted.org/packages/3a/1f/388c87607eb6c7abee4866355eeebb6221b3bcb413a64ce4a5cfc3edd22f/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a95754928d47b37ea5c4b5180101db2afabef1da0dd5cd0c573b547d5d036ff0", size = 4507170, upload-time = "2026-10-02T23:38:29.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/73/5515a3f555d31748564fbeb0b9e0b6ab7a647bf2ea0cdb6d233f20f710e3/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7606b413730ac6002930faa96f52195ed6969040b78071a3336370661f5e9e09", size = 4301439, upload-time = "2026-10-02T23:46:47.845Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a0/0eae9b96f6f8a09f3e5d388f6fc5d6fe8d5ff2aced2a5c75bea7333d445e/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76c3d5c7ada0925ff12e8c2395c547f88fcfccc13368ff8e8dd55ab5e28dcdab", size = 4594441, upload-time = "2026-10-03T02:46:16.928Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b1/b6d681015414ce9048c07ef3ac49b693c1b8373de4716ee4bdeff4ec19a0/sqlalchemy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:cb6fc5c856e8f1c0228bf74b636c0a03a6c12dd502ced7aa82ced21dcdc7f547", size = 2371547, upload-time = "2026-10-03T02:49:23.549Z" },
{ url = "https://files.pythonhosted.org/packages/56/ad/35aa4d737016640c6da7573a4bcd9d91b59a87359bc6df075627963a4781/sqlalchemy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:82d4c63d338737d593f1d29c4dae0a124577a9a76186d90b5c0f9ec9cbeeab35", size = 2421558, upload-time = "2026-10-03T02:49:25.448Z" },
{ url = "https://files.pythonhosted.org/packages/fc/42/0dcc7cd92a3f4773376a5488b6ce0d6d75d2d029ad7e7f9e0ecc5fb48fcd/sqlalchemy-2.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:0ab6f42761cc5c48d2f3e8a70810d10630d8222a51a7111cf9b3a48f22c3f148", size = 2382507, upload-time = "2026-10-03T02:42:42.812Z" },
{ url = "https://files.pythonhosted.org/packages/e5/12/23d8fb7ed90e5b674006ace415ede1c60cdfc38ace50968b59ef42c22754/sqlalchemy-2.1.3-py3-none-any.whl", hash = "sha256:6f0cf4debd86cb5623a50310784bd005c215761ca7f618d0d4f46193d033675a", size = 2052603, upload-time = "2026-10-03T02:34:19.115Z" },
]
[package.optional-dependencies]
asyncio = [
{ name = "greenlet" },
]
[[package]]
name = "starlette"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" },
]
[[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.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
]
[[package]]
name = "uvicorn"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
]
[[package]]
name = "yarl"
version = "1.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" },
{ url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" },
{ url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" },
{ url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" },
{ url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" },
{ url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" },
{ url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" },
{ url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" },
{ url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" },
{ url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" },
{ url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" },
{ url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" },
{ url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" },
{ url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" },
]
+14
View File
@@ -0,0 +1,14 @@
# 跨服务契约
`schemas/` 保存版本化 JSON Schema,`openapi/` 保存平台 API 快照。
共享合法/非法样例位于 `tests/fixtures/contracts/`,Python 与 Rust 必须共同验证。
目前只固定最小判题通知:`schema_version=1`、`kind=judge.evaluation`、UUID 格式的
`evaluation_id`。它不包含源码、隐藏数据、租约或执行授权;消费者仍须向协调器领取。
未知版本、未知类型及未知字段均拒绝。
`make contracts` 从 Python 模型生成 Schema/OpenAPI,并生成前端 API 类型。
`make check` 检查快照未过期及两种语言对样例的接受/拒绝一致。
执行请求/报告、评测状态机、租约与结果提交契约在 JS-01/DR-01 中逐项补充。
生成器 `export.py` 是开发工具,允许读取 API 组合层;运行期的契约模型禁止依赖业务模块。
+62
View File
@@ -0,0 +1,62 @@
{
"components": {
"schemas": {
"HealthResponse": {
"additionalProperties": false,
"properties": {
"service": {
"const": "platform",
"title": "Service",
"type": "string"
},
"status": {
"const": "ok",
"title": "Status",
"type": "string"
},
"version": {
"title": "Version",
"type": "string"
}
},
"required": [
"status",
"service",
"version"
],
"title": "HealthResponse",
"type": "object"
}
}
},
"info": {
"description": "工程骨架。当前只提供进程存活检查,业务接口逐项实现。",
"title": "KF-OnlineJudge Platform API",
"version": "0.0.1"
},
"openapi": "3.1.0",
"paths": {
"/api/v1/health/live": {
"get": {
"description": "Check process liveness only; does not attest DB, queue or judge readiness.",
"operationId": "liveness",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthResponse"
}
}
},
"description": "Successful Response"
}
},
"summary": "Liveness",
"tags": [
"health"
]
}
}
}
}
@@ -0,0 +1,30 @@
{
"$id": "urn:kf-oj:judge-task:v1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"description": "A wake-up notification, not execution authority or the source of task state.",
"properties": {
"evaluation_id": {
"format": "uuid",
"title": "Evaluation Id",
"type": "string"
},
"kind": {
"const": "judge.evaluation",
"title": "Kind",
"type": "string"
},
"schema_version": {
"const": 1,
"title": "Schema Version",
"type": "integer"
}
},
"required": [
"schema_version",
"kind",
"evaluation_id"
],
"title": "JudgeTaskNotification",
"type": "object"
}
+10
View File
@@ -0,0 +1,10 @@
# Development-only values; copy to deploy/.env before make infra-up.
POSTGRES_USER=kf_oj
POSTGRES_PASSWORD=local-dev-only
POSTGRES_DB=kf_oj
POSTGRES_PORT=5432
RABBITMQ_USER=kf_oj
RABBITMQ_PASSWORD=local-dev-only
RABBITMQ_VHOST=kf_oj
RABBITMQ_PORT=5672
RABBITMQ_MANAGEMENT_PORT=15672
+21
View File
@@ -0,0 +1,21 @@
# 本地开发依赖
`compose.dev.yml` 只提供 PostgreSQL 和 RabbitMQ,端口绑定 localhost。
先复制 `.env.example` 为 `.env`,然后在仓库根目录运行:
```bash
make infra-config
make infra-up
make infra-down
```
`infra-down` 保留数据卷;不要把示例口令用于正式部署。
API 存活检查和前端预览不依赖这些容器,可以先独立启动。
该 Compose 不运行判题 Worker 或不可信代码。
镜像固定具体维护版本(PostgreSQL 18.6、RabbitMQ 4.3.6),正式实验前另锁镜像摘要。
配置检查只验证 Compose 结构;成功启动、实际健康与备份恢复需要单独验证。
后续受控主机服务和最小权限沙箱启动器配置放在本目录。
官方镜像说明:[PostgreSQL](https://hub.docker.com/_/postgres)、
[RabbitMQ](https://hub.docker.com/_/rabbitmq)。
+40
View File
@@ -0,0 +1,40 @@
name: kf-oj-dev
services:
postgres:
image: postgres:18.6-bookworm
environment:
POSTGRES_USER: ${POSTGRES_USER:?required}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
POSTGRES_DB: ${POSTGRES_DB:?required}
ports:
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 20
mem_limit: 1g
cpus: 1
rabbitmq:
image: rabbitmq:4.3.6-management
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:?required}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?required}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:?required}
ports:
- "127.0.0.1:${RABBITMQ_PORT:-5672}:5672"
- "127.0.0.1:${RABBITMQ_MANAGEMENT_PORT:-15672}:15672"
volumes:
- rabbitmq-data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 5s
timeout: 5s
retries: 20
mem_limit: 768m
cpus: 1
volumes:
postgres-data:
rabbitmq-data:
+7
View File
@@ -0,0 +1,7 @@
# 可复现实验
后续负载生成、调度对照、故障注入和统计脚本放在本目录;工程骨架阶段未开始性能实验。
每次实验保存 Git 提交、环境、配置、负载摘要、原始事件、结果和复现命令。
`results/` 默认忽略,报告只提交去标识摘要和原始材料的复现索引。
每项实验先明确条件、正确性检查和验收指标,再运行并保留复现材料。
+3
View File
@@ -0,0 +1,3 @@
VITE_API_BASE_URL=
# Development proxy only; never bundled into the browser as a credential.
KF_API_PROXY_TARGET=http://127.0.0.1:8000
+1
View File
@@ -0,0 +1 @@
26.10.0
+2
View File
@@ -0,0 +1,2 @@
# pnpm 11 reads project settings from pnpm-workspace.yaml.
# Keep this file for future registry/auth configuration only.
+4
View File
@@ -0,0 +1,4 @@
pnpm-lock.yaml
dist
node_modules
src/api/generated
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
+27
View File
@@ -0,0 +1,27 @@
# Vue 前端工程
Vue 3 + TypeScript + Vite + Element Plus,使用 Vue Router 与 Pinia。
当前页面是开发预览和实际 API 连接检查;没有模拟登录或模拟评测结果。
```bash
pnpm install --frozen-lockfile
pnpm dev
pnpm typecheck
pnpm test
pnpm build
```
开发服务在 `http://127.0.0.1:5173`,`/api` 代理到 `http://127.0.0.1:8000`。
复制 `.env.example` 为 `.env` 可调整代理和 API 地址。
生产部署须配置 SPA 路由回退及 `/api` 反向代理;开发代理不进入生产产物。
- `views/`:路由页面;`layouts/`:页面框架;`components/`:可复用界面组件。
- `api/`:HTTP 客户端和业务 API 封装;`api/generated/`:从 OpenAPI 生成的类型。
- `composables/`:带生命周期的交互逻辑;`stores/`:跨页面共享状态。
- `router/`:路由;`styles/`:全局样式。
已有代码按实际需要落位;`components/` 和 `stores/` 是后续扩展位置,不放空状态实现。
API 类型更新执行根目录 `make contracts`;前端通过 `pnpm check:api` 拒绝过期类型。
登录凭据、模型密钥和数据库连接不能进入 `VITE_` 环境变量。
`pnpm test` 验证代码草稿在刷新、切到后台与页面离开时的保存行为,已纳入 `make check-frontend`。
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KF 管理控制台</title>
</head>
<body>
<div id="admin-app"></div>
<script type="module" src="/src/admin/main.ts"></script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import vue from 'eslint-plugin-vue'
import prettier from 'eslint-config-prettier/flat'
export default tseslint.config(
{ ignores: ['dist/**', 'node_modules/**', 'src/api/generated/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
...vue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } },
// TypeScript checks identifiers and DOM globals in Vue's script setup.
rules: { 'vue/multi-word-component-names': 'off', 'no-undef': 'off' },
},
{
files: ['vite.config.ts'],
languageOptions: { globals: { process: 'readonly' } },
},
prettier,
)
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#2563eb" />
<title>KF-OnlineJudge</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
{
"name": "kf-oj-frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"packageManager": "pnpm@11.28.4",
"engines": {
"node": ">=24.12.0 <27"
},
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --host 127.0.0.1",
"typecheck": "vue-tsc --noEmit",
"test": "node --test tests/*.test.ts",
"lint": "eslint . --max-warnings 0",
"format": "prettier --write .",
"format:check": "prettier --check .",
"generate:api": "openapi-typescript ../contracts/openapi/platform.v1.json -o src/api/generated/schema.d.ts",
"check:api": "pnpm generate:api --check"
},
"dependencies": {
"@element-plus/icons-vue": "2.3.2",
"@misskey-dev/tagcanvas-es": "0.1.2",
"element-plus": "^2.9.0",
"monaco-editor": "0.57.0",
"openapi-fetch": "^0.14.0",
"pinia": "^3.0.0",
"vue": "^3.5.0",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^10.0.0",
"@types/node": "^24.0.0",
"@vitejs/plugin-vue": "^6.0.0",
"eslint": "^10.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-vue": "^10.0.0",
"openapi-typescript": "^7.0.0",
"prettier": "^3.0.0",
"typescript": "~5.9.0",
"typescript-eslint": "^8.0.0",
"vite": "^7.0.0",
"vue-tsc": "^3.0.0"
}
}
+2398
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
saveExact: true
verifyDepsBeforeRun: false
allowBuilds:
esbuild: true
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 440" fill="none">
<defs><linearGradient id="sky" x1="0" y1="0" x2="1200" y2="440" gradientUnits="userSpaceOnUse"><stop stop-color="#eff7ff"/><stop offset="1" stop-color="#c9e3fa"/></linearGradient><linearGradient id="veil"><stop stop-color="#edf6ff"/><stop offset=".53" stop-color="#edf6ff" stop-opacity=".9"/><stop offset="1" stop-color="#edf6ff" stop-opacity="0"/></linearGradient></defs>
<path fill="url(#sky)" d="M0 0h1200v440H0z"/><circle cx="973" cy="100" r="66" fill="#fff" opacity=".75"/>
<path d="m350 350 182-211 197 203 107-138 136 144 92-94 136 117v69H350z" fill="#b8d6ee"/><path d="m532 139 58 60-55-17-43 28z" fill="#ecf7ff"/><path d="m836 204 51 53-51-14-30 20z" fill="#e6f4ff"/>
<path d="M0 394c132-50 237-24 380-47s217-3 359 22 296-28 461-55v126H0z" fill="#aacbe5"/><path d="M0 414c135-28 304-1 506-11s414 3 694-39v76H0z" fill="#dcecf7"/>
<path d="M836 226h183v118H836z" fill="#f5faff"/><path d="m821 226 106-51 106 51z" fill="#79a9d2"/><path d="M905 271h41v73h-41z" fill="#a4c7e3"/><path d="M851 249h28v28h-28zm123 0h28v28h-28z" fill="#c2dcef"/>
<path d="M772 342h19v-67h-19z" fill="#779cbc"/><path d="m737 290 46-118 46 118z" fill="#8ab6cc"/><path d="m730 316 53-112 54 112z" fill="#77a6bc"/>
<path d="M1082 350h18v-93h-18z" fill="#779cbc"/><path d="m1035 291 56-131 57 131z" fill="#8cb8cf"/><path d="m1023 321 68-127 67 127z" fill="#77a6bf"/>
<path d="M883 355c-20 36-65 45-111 57" stroke="#f5faff" stroke-width="14" stroke-linecap="round"/>
<g stroke="#91bce0" opacity=".6"><rect x="674" y="81" width="114" height="75" rx="8" fill="#f3f9ff" transform="rotate(-9 674 81)"/><path d="m698 105-9 10 12 7m29-18 12 7-9 10m-6-22-8 26" stroke-width="3"/><rect x="1053" y="42" width="73" height="49" rx="6" fill="#f4faff" transform="rotate(7 1053 42)"/><path d="M1069 59h38m-38 10h26" stroke-width="3"/></g><path d="M0 0h1200v440H0z" fill="url(#veil)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="340" viewBox="0 0 500 340">
<defs><pattern id="dots" width="24" height="24" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1" fill="#b6d2ef"/></pattern><linearGradient id="paper" x2="0.8" y2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#f5faff"/></linearGradient></defs>
<rect x="28" y="12" width="444" height="308" rx="28" fill="url(#dots)"/>
<path d="M92 212v57h85v-43h97v49h140v-104" fill="none" stroke="#c8dff6" stroke-width="20" stroke-linejoin="round"/>
<path d="M92 212v57h85v-43h97v49h140v-104" fill="none" stroke="#76b5ee" stroke-width="2" stroke-dasharray="5 7"/>
<g transform="rotate(-5 231 118)"><rect x="73" y="30" width="308" height="183" rx="16" fill="#cfdef0" opacity=".4"/><rect x="65" y="22" width="308" height="183" rx="16" fill="url(#paper)" stroke="#c8dff6"/><path d="M65 60h308" stroke="#e2edf8"/><circle cx="86" cy="41" r="4" fill="#b9d6f2"/><circle cx="101" cy="41" r="4" fill="#d4e5f6"/><circle cx="116" cy="41" r="4" fill="#e2edf8"/><text x="145" y="45" font-family="monospace" font-size="10" fill="#8fa8c4">solution.cpp</text><text x="89" y="92" font-family="monospace" font-size="14" fill="#657e9e">int main() {</text><text x="106" y="122" font-family="monospace" font-size="14" fill="#409eff">cin &gt;&gt; a &gt;&gt; b;</text><text x="106" y="151" font-family="monospace" font-size="14" fill="#455e80">cout &lt;&lt; a + b;</text><text x="89" y="181" font-family="monospace" font-size="14" fill="#657e9e">}</text></g>
<g transform="rotate(6 377 185)"><rect x="307" y="144" width="151" height="82" rx="12" fill="#fff" stroke="#b6d2ef"/><text x="329" y="169" font-family="monospace" font-size="9" fill="#8fa8c4">01 / SIMPLE START</text><text x="329" y="204" font-family="monospace" font-size="25" font-weight="600" fill="#409eff">1 + 2 = 3</text></g>
<circle cx="92" cy="269" r="13" fill="#fff" stroke="#b9d6f2" stroke-width="2"/><text x="87" y="273" font-family="monospace" font-size="12" fill="#76a9de">A</text><circle cx="274" cy="275" r="13" fill="#409eff"/><path d="m268 275 4 4 8-9" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/><circle cx="414" cy="275" r="7" fill="#fff" stroke="#b9d6f2" stroke-width="2"/>
<text x="184" y="313" font-family="monospace" font-size="10" letter-spacing="3" fill="#8fa8c4">THINK / BUILD / REPEAT</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
import AppLayout from './layouts/AppLayout.vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<template>
<el-config-provider :locale="zhCn">
<AppLayout>
<RouterView v-slot="{ Component, route }">
<component :is="Component" :key="route.path" />
</RouterView>
</AppLayout>
</el-config-provider>
</template>
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import UiIcon from '../components/UiIcon.vue'
import { adminSections } from './navigation'
const route = useRoute()
</script>
<template>
<el-config-provider :locale="zhCn">
<div class="admin-application">
<header class="admin-app-header">
<a href="/admin/index.html" class="brand"
><span class="brand-mark">KF.</span
><span class="brand-name">管理控制台<small>ADMIN CONSOLE</small></span></a
>
<span class="admin-app-label">独立管理应用 · 界面预览</span>
<a href="/" class="admin-return">返回前台 <UiIcon name="arrow" /></a>
</header>
<nav class="admin-app-nav" aria-label="管理导航">
<RouterLink
v-for="section in adminSections"
:key="section.id"
:to="section.id === 'overview' ? '/' : '/' + section.id"
:aria-label="section.title"
:class="{ active: (route.params.section || 'overview') === section.id }"
><UiIcon :name="section.icon" /><span>{{ section.title }}</span></RouterLink
>
</nav>
<main class="admin-app-main">
<RouterView v-slot="{ Component, route: currentRoute }"
><component :is="Component" :key="currentRoute.path"
/></RouterView>
</main>
<footer class="admin-app-footer">
KF OnlineJudge / 管理控制台<span>示例数据 · 管理员授权与业务接口待接入</span>
</footer>
</div>
</el-config-provider>
</template>
+272
View File
@@ -0,0 +1,272 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { problems, contests, rankings } from '../data/preview'
import PageHeading from '../components/PageHeading.vue'
import PanelCard from '../components/PanelCard.vue'
import SubmissionChart from '../components/SubmissionChart.vue'
import EmptyState from '../components/EmptyState.vue'
import { adminSections as sections } from './navigation'
const route = useRoute()
const section = computed(() => String(route.params.section || 'overview'))
const current = computed(() => sections.find((s) => s.id === section.value))
const query = ref('')
const problemItems = computed(() => problems.filter((p) => (p.id + p.title).includes(query.value)))
const contestItems = computed(() => contests.filter((c) => c.title.includes(query.value)))
const userItems = computed(() =>
rankings.filter((u) => u.username.toLowerCase().includes(query.value.toLowerCase())),
)
const editOpen = ref(false)
const draft = reactive({ id: '', title: '', difficulty: '入门', status: '公开' })
const settings = reactive({
name: 'KF OnlineJudge',
slogan: '每一次提交,都有新的收获。',
registration: true,
announcement: '欢迎来到 KF OnlineJudge',
filing: '',
})
const nodes = [
{
name: 'home-worker-01',
host: 'Debian',
capacity: '4 核 / 8 GB',
status: '在线',
load: 32,
tasks: 2,
},
{
name: 'cloud-worker-01',
host: 'Ubuntu',
capacity: '8 核 / 8 GB',
status: '在线',
load: 46,
tasks: 4,
},
{
name: 'cloud-worker-02',
host: 'Ubuntu',
capacity: '2 核 / 2 GB',
status: '离线',
load: 0,
tasks: 0,
},
]
function editProblem(id?: string) {
const row = problems.find((problem) => problem.id === id)
Object.assign(draft, {
id: row?.id ?? '',
title: row?.title ?? '',
difficulty: row?.difficulty ?? '入门',
status: '公开',
})
editOpen.value = true
}
</script>
<template>
<PageHeading
title="管理中心"
eyebrow="ADMINISTRATION"
description="管理界面预览 · 管理员身份与写入接口待接入。"
><a href="/"><el-button>返回站点</el-button></a></PageHeading
>
<div class="admin-shell">
<div v-if="current" class="stack">
<template v-if="section === 'overview'"
><div class="summary-grid">
<div class="summary-card">
<p>公开题目</p>
<strong>248</strong><small>示例统计</small>
</div>
<div class="summary-card">
<p>累计提交</p>
<strong>18,240</strong><small>示例统计</small>
</div>
<div class="summary-card">
<p>注册用户</p>
<strong>328</strong><small>示例统计</small>
</div>
<div class="summary-card">
<p>在线节点</p>
<strong>2 / 3</strong><small>示例统计</small>
</div>
</div>
<PanelCard title="提交趋势" icon="rankings"><SubmissionChart /></PanelCard
><PanelCard title="开发接入状态" icon="connection"
><el-descriptions :column="1" border
><el-descriptions-item label="当前界面">前端预览</el-descriptions-item
><el-descriptions-item label="账号与权限">待接入</el-descriptions-item
><el-descriptions-item label="评测与节点状态">待接入</el-descriptions-item
><el-descriptions-item label="管理写入与操作审计"
>待接入</el-descriptions-item
></el-descriptions
></PanelCard
></template
>
<PanelCard
v-else-if="section === 'problems'"
title="题目管理"
icon="problems"
subtitle="示例题库"
><template #actions
><el-button size="small" type="primary" @click="editProblem()"
>新建题目草稿</el-button
></template
>
<div class="filters">
<el-input
v-model="query"
placeholder="搜索题号或名称"
aria-label="搜索管理题目"
clearable
/>
</div>
<el-table :data="problemItems" stripe
><el-table-column label="题号" prop="id" width="80" /><el-table-column
label="名称"
prop="title"
min-width="190"
/><el-table-column label="难度" prop="difficulty" width="90" /><el-table-column
label="更新日期"
prop="updated"
width="130"
/><el-table-column label="操作" width="150"
><template #default="{ row }"
><el-button link type="primary" @click="editProblem(row.id)">编辑预览</el-button
><a :href="'/problems/' + row.id" class="text-link" style="margin-left: 10px"
>查看</a
></template
></el-table-column
></el-table
></PanelCard
>
<PanelCard
v-else-if="section === 'contests'"
title="比赛管理"
icon="contests"
subtitle="示例赛程"
><template #actions
><el-button size="small" type="primary" disabled>创建比赛</el-button></template
>
<div class="filters">
<el-input v-model="query" placeholder="搜索比赛" aria-label="搜索管理比赛" clearable />
</div>
<el-table :data="contestItems" stripe
><el-table-column label="比赛名称" prop="title" min-width="220" /><el-table-column
label="赛制"
prop="type"
width="75"
/><el-table-column label="状态" prop="status" width="120" /><el-table-column
label="开始时间"
prop="start"
min-width="165"
/><el-table-column label="操作" width="90"
><template #default="{ row }"
><a class="text-link" :href="'/contests/' + row.id">查看</a></template
></el-table-column
></el-table
></PanelCard
>
<PanelCard v-else-if="section === 'users'" title="用户管理" icon="teams" subtitle="示例用户"
><div class="filters">
<el-input v-model="query" placeholder="搜索用户名" aria-label="搜索管理用户" clearable />
</div>
<el-table :data="userItems" stripe
><el-table-column label="用户名" prop="username" min-width="170" /><el-table-column
label="通过题目"
prop="solved"
width="100"
/><el-table-column label="竞赛积分" prop="rating" width="110" /><el-table-column
label="角色"
width="90"
><template #default>用户</template></el-table-column
><el-table-column label="操作" width="160"
><template #default="{ row }"
><a class="text-link" :href="'/users/' + row.username">查看主页</a
><el-button link disabled style="margin-left: 12px">修改角色</el-button></template
></el-table-column
></el-table
></PanelCard
>
<PanelCard
v-else-if="section === 'judges'"
title="判题节点"
icon="connection"
subtitle="示例监控数据"
><p class="notice-strip">实际节点、负载、心跳和任务状态将在协调器接口接入后展示。</p>
<el-table :data="nodes" stripe
><el-table-column label="节点" prop="name" min-width="170" /><el-table-column
label="系统"
prop="host"
width="100"
/><el-table-column label="资源" prop="capacity" min-width="135" /><el-table-column
label="状态"
width="90"
><template #default="{ row }"
><span class="status-dot" :class="{ offline: row.status === '离线' }" />{{
row.status
}}</template
></el-table-column
><el-table-column label="负载" min-width="140"
><template #default="{ row }"
><el-progress :percentage="row.load" :stroke-width="5" /></template></el-table-column
><el-table-column label="任务" prop="tasks" width="70" /><el-table-column
label="操作"
width="100"
><template #default
><el-button link disabled>暂停接单</el-button></template
></el-table-column
></el-table
>
<div class="comment-placeholder">
队列积压、租约过期、重试记录与故障恢复操作待接入。
</div></PanelCard
>
<PanelCard v-else-if="section === 'settings'" title="站点设置" icon="settings"
><el-form label-position="top" style="max-width: 700px" @submit.prevent
><el-form-item label="站点名称"><el-input v-model="settings.name" /></el-form-item
><el-form-item label="站点标语"><el-input v-model="settings.slogan" /></el-form-item
><el-form-item label="首页公告"
><el-input v-model="settings.announcement" type="textarea" :rows="3" /></el-form-item
><el-form-item label="备案信息"
><el-input
v-model="settings.filing"
placeholder="上线前填写真实备案信息" /></el-form-item
><el-form-item label="允许注册"
><el-switch v-model="settings.registration"
/></el-form-item>
<div class="form-actions">
<el-button type="primary" disabled>保存设置</el-button
><span class="muted">当前编辑只在本页面预览</span>
</div></el-form
></PanelCard
>
</div>
<EmptyState v-else title="管理页面不存在" />
</div>
<el-dialog
v-model="editOpen"
:title="draft.id ? '编辑题目 · ' + draft.id : '新建题目草稿'"
width="min(600px, 92vw)"
><el-form label-position="top"
><el-form-item label="题目名称"
><el-input v-model="draft.title" placeholder="填写题目名称" /></el-form-item
><el-form-item label="难度"
><el-select v-model="draft.difficulty"
><el-option
v-for="d in ['入门', '简单', '中等', '困难']"
:key="d"
:label="d"
:value="d" /></el-select></el-form-item
><el-form-item label="发布状态"
><el-radio-group v-model="draft.status"
><el-radio value="公开">公开</el-radio
><el-radio value="草稿">草稿</el-radio></el-radio-group
></el-form-item
>
<p class="small muted">题面编辑、测试数据和版本发布将在题库接口接入后启用。</p></el-form
><template #footer
><el-button @click="editOpen = false">关闭预览</el-button
><el-button type="primary" disabled>保存题目</el-button></template
></el-dialog
>
</template>
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import { registerUi } from '../plugins/ui'
import { adminRouter } from './router'
import AdminApp from './AdminApp.vue'
import '../styles/base.css'
import '../styles/blueprint.css'
const app = createApp(AdminApp)
registerUi(app)
app.use(adminRouter).mount('#admin-app')
+8
View File
@@ -0,0 +1,8 @@
export const adminSections = [
{ id: 'overview', title: '概览', icon: 'monitor' },
{ id: 'problems', title: '题目管理', icon: 'problems' },
{ id: 'contests', title: '比赛管理', icon: 'contests' },
{ id: 'users', title: '用户管理', icon: 'teams' },
{ id: 'judges', title: '判题节点', icon: 'connection' },
{ id: 'settings', title: '站点设置', icon: 'settings' },
]
+12
View File
@@ -0,0 +1,12 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { adminSections } from './navigation'
export const adminRouter = createRouter({
history: createWebHashHistory('/admin/index.html'),
scrollBehavior: () => ({ top: 0 }),
routes: [{ path: '/:section?', component: () => import('./AdminView.vue') }],
})
adminRouter.afterEach((to) => {
const section = adminSections.find((item) => item.id === (to.params.section || 'overview'))
document.title = `${section?.title ?? '页面不存在'} · KF 管理控制台`
})
+6
View File
@@ -0,0 +1,6 @@
import createClient from 'openapi-fetch'
import type { paths } from './generated/schema'
export const api = createClient<paths>({
baseUrl: import.meta.env.VITE_API_BASE_URL || '',
})
+75
View File
@@ -0,0 +1,75 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/api/v1/health/live": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Liveness
* @description Check process liveness only; does not attest DB, queue or judge readiness.
*/
get: operations["liveness"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** HealthResponse */
HealthResponse: {
/**
* Service
* @constant
*/
service: "platform";
/**
* Status
* @constant
*/
status: "ok";
/** Version */
version: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
liveness: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HealthResponse"];
};
};
};
};
}
+9
View File
@@ -0,0 +1,9 @@
import { api } from './client'
export async function fetchLiveness(signal: AbortSignal) {
const { data, error, response } = await api.GET('/api/v1/health/live', { signal })
if (!response.ok || error || !data) {
throw new Error(`平台暂时无法连接(HTTP ${response.status})`)
}
return data
}
+84
View File
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import * as monaco from 'monaco-editor/editor/editor.api'
import 'monaco-editor/features/bracketMatching/register'
import 'monaco-editor/features/clipboard/register'
import 'monaco-editor/features/comment/register'
import 'monaco-editor/features/contextmenu/register'
import 'monaco-editor/features/find/register'
import 'monaco-editor/features/linesOperations/register'
import 'monaco-editor/features/wordOperations/register'
import 'monaco-editor/languages/definitions/cpp/register'
import 'monaco-editor/languages/definitions/python/register'
import 'monaco-editor/languages/definitions/rust/register'
import EditorWorker from 'monaco-editor/editor/editor.worker?worker'
const props = defineProps<{
modelValue: string
language: string
theme: string
fontSize: number
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const container = ref<HTMLDivElement>()
let editor: monaco.editor.IStandaloneCodeEditor | undefined
let model: monaco.editor.ITextModel | undefined
let subscription: monaco.IDisposable | undefined
;(
globalThis as typeof globalThis & { MonacoEnvironment?: { getWorker: () => Worker } }
).MonacoEnvironment = { getWorker: () => new EditorWorker() }
onMounted(() => {
if (!container.value) return
model = monaco.editor.createModel(props.modelValue, props.language)
editor = monaco.editor.create(container.value, {
model,
theme: props.theme,
fontSize: props.fontSize,
minimap: { enabled: false },
automaticLayout: true,
scrollBeyondLastLine: false,
padding: { top: 18 },
tabSize: 4,
ariaLabel: '代码编辑器',
lineNumbersMinChars: 3,
wordWrap: 'on',
renderLineHighlight: 'line',
contextmenu: true,
})
subscription = editor.onDidChangeModelContent(() => {
emit('update:modelValue', editor!.getValue())
})
})
watch(
() => props.modelValue,
(value) => {
if (model && value !== model.getValue()) model.setValue(value)
},
)
watch(
() => props.language,
(value) => {
if (model) monaco.editor.setModelLanguage(model, value)
},
)
watch(
() => props.theme,
(value) => monaco.editor.setTheme(value),
)
watch(
() => props.fontSize,
(value) => editor?.updateOptions({ fontSize: value }),
)
onBeforeUnmount(() => {
subscription?.dispose()
editor?.dispose()
model?.dispose()
})
</script>
<template><div ref="container" class="monaco-container" /></template>
<style scoped>
.monaco-container {
width: 100%;
height: 100%;
min-height: 320px;
}
</style>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { Difficulty } from '../data/preview'
defineProps<{ value: Difficulty | string }>()
</script>
<template>
<span
class="difficulty"
:class="{
'difficulty-easy': value === '入门' || value === '简单',
'difficulty-medium': value === '中等',
'difficulty-hard': value === '困难',
}"
>{{ value }}</span
>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string }>()
</script>
<template>
<div class="empty-state">
<el-empty :description="title" :image-size="80" />
<p v-if="description" class="muted">{{ description }}</p>
<slot />
</div>
</template>
+13
View File
@@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string; eyebrow?: string }>()
</script>
<template>
<header class="page-heading">
<div>
<p v-if="eyebrow" class="eyebrow">{{ eyebrow }}</p>
<h1>{{ title }}</h1>
<p v-if="description" class="muted">{{ description }}</p>
</div>
<div class="page-heading-actions"><slot /></div>
</header>
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import UiIcon from './UiIcon.vue'
defineProps<{ title: string; icon?: string; subtitle?: string }>()
</script>
<template>
<section class="panel">
<header class="panel-heading">
<h2>
<UiIcon v-if="icon" :name="icon" />{{ title }}<small v-if="subtitle">{{ subtitle }}</small>
</h2>
<div class="panel-actions"><slot name="actions" /></div>
</header>
<div class="panel-body"><slot /></div>
</section>
</template>
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { acceptance, type Problem } from '../data/preview'
import DifficultyTag from './DifficultyTag.vue'
defineProps<{ items: Problem[]; compact?: boolean }>()
</script>
<template>
<el-table :data="items" stripe style="width: 100%" empty-text="没有符合条件的题目">
<el-table-column prop="id" label="题号" width="90"
><template #default="{ row }"
><span class="mono muted">{{ row.id }}</span></template
></el-table-column
>
<el-table-column label="题目" min-width="190"
><template #default="{ row }"
><RouterLink class="table-link" :to="`/problems/${row.id}`">{{
row.title
}}</RouterLink></template
></el-table-column
>
<el-table-column v-if="!compact" label="标签" min-width="180"
><template #default="{ row }"
><div class="tag-list">
<span v-for="tag in row.tags" :key="tag" class="tag">{{ tag }}</span>
</div></template
></el-table-column
>
<el-table-column label="难度" width="90"
><template #default="{ row }"><DifficultyTag :value="row.difficulty" /></template
></el-table-column>
<el-table-column label="通过率" width="100" align="right"
><template #default="{ row }"
><span class="muted tabular"
>{{ acceptance({ accepted: row.accepted, submissions: row.submissions }) }}%</span
></template
></el-table-column
>
<el-table-column v-if="!compact" prop="updated" label="更新日期" width="125" />
</el-table>
</template>
+150
View File
@@ -0,0 +1,150 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
const showAccepted = ref(true)
const showTotal = ref(true)
const points = [
{ day: '09-28', total: 44, accepted: 28 },
{ day: '09-29', total: 68, accepted: 43 },
{ day: '09-30', total: 52, accepted: 32 },
{ day: '10-01', total: 91, accepted: 57 },
{ day: '10-02', total: 77, accepted: 46 },
{ day: '10-03', total: 116, accepted: 74 },
{ day: '10-04', total: 96, accepted: 62 },
]
const active = ref<number | null>(null)
const x = (index: number) => 55 + index * 111
const y = (value: number) => 192 - (value / 120) * 155
const line = (field: 'total' | 'accepted') =>
points.map((point, index) => x(index) + ',' + y(point[field])).join(' ')
const selected = computed(() => (active.value === null ? undefined : points[active.value]))
</script>
<template>
<div class="submission-chart">
<div class="chart-legend">
<button :aria-pressed="showAccepted" @click="showAccepted = !showAccepted">
<span class="chart-dot accepted" />通过</button
><button :aria-pressed="showTotal" @click="showTotal = !showTotal">
<span class="chart-dot total" />总提交</button
><span class="muted">单位:次 · 示例统计</span>
</div>
<svg
viewBox="0 0 770 240"
role="img"
aria-label="近七天示例提交趋势:总提交44、68、52、91、77、116、96次"
>
<g v-for="tick in [0, 30, 60, 90, 120]" :key="tick">
<line x1="55" x2="721" :y1="y(tick)" :y2="y(tick)" stroke="#edf1f6" />
<text x="42" :y="y(tick) + 4" text-anchor="end">{{ tick }}</text>
</g>
<g v-for="(point, index) in points" :key="point.day">
<text :x="x(index)" y="219" text-anchor="middle">{{ point.day }}</text>
</g>
<polyline
v-if="showTotal"
:points="line('total')"
fill="none"
stroke="#76b5ee"
stroke-width="2"
stroke-linejoin="round"
/>
<polyline
v-if="showAccepted"
:points="line('accepted')"
fill="none"
stroke="#8ccdab"
stroke-width="2"
stroke-linejoin="round"
/>
<g v-for="(point, index) in points" :key="index">
<circle
v-if="showTotal"
:cx="x(index)"
:cy="y(point.total)"
r="3"
fill="white"
stroke="#76b5ee"
/>
<circle
v-if="showAccepted"
:cx="x(index)"
:cy="y(point.accepted)"
r="3"
fill="white"
stroke="#8ccdab"
/>
<rect
:x="x(index) - 20"
y="25"
width="40"
height="174"
fill="transparent"
@mouseenter="active = index"
@mouseleave="active = null"
>
<title>{{ point.day }}:总提交 {{ point.total }},通过 {{ point.accepted }}</title>
</rect>
</g>
</svg>
<p class="chart-detail" aria-live="polite">
{{
selected
? selected.day + ' · 总提交 ' + selected.total + ' 次 · 通过 ' + selected.accepted + ' 次'
: '近 7 天的提交与通过情况'
}}
</p>
</div>
</template>
<style scoped>
.chart-legend {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
font-size: 11px;
margin-bottom: 10px;
}
.chart-legend button {
border: 0;
background: none;
color: #8797ac;
font-size: 12px;
display: flex;
gap: 7px;
align-items: center;
padding: 3px;
}
.chart-legend button[aria-pressed='false'] {
opacity: 0.4;
}
.chart-dot {
width: 7px;
height: 7px;
border-radius: 50%;
display: inline-block;
}
.accepted {
background: #8ccdab;
}
.total {
background: #76b5ee;
}
.submission-chart svg {
display: block;
width: 100%;
height: auto;
}
.submission-chart text {
font:
11px Inter,
'Microsoft YaHei',
sans-serif;
fill: #9aa8b9;
}
.chart-detail {
text-align: center;
color: #9cabbd;
font-size: 11px;
min-height: 20px;
}
</style>
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { findProblem, type submissions } from '../data/preview'
import VerdictTag from './VerdictTag.vue'
defineProps<{ items: typeof submissions; compact?: boolean }>()
</script>
<template>
<el-table :data="items" stripe empty-text="暂无符合条件的评测记录"
><el-table-column label="提交编号" width="100"
><template #default="{ row }"
><RouterLink class="table-link mono" :to="'/submissions/' + row.id"
>#{{ row.id }}</RouterLink
></template
></el-table-column
><el-table-column label="题目" min-width="180"
><template #default="{ row }"
><RouterLink class="table-link" :to="'/problems/' + row.problemId">{{
findProblem(row.problemId)?.title
}}</RouterLink></template
></el-table-column
><el-table-column v-if="!compact" label="用户" min-width="120"
><template #default="{ row }"
><RouterLink :to="'/users/' + row.username">{{ row.username }}</RouterLink></template
></el-table-column
><el-table-column label="结果" width="110"
><template #default="{ row }"><VerdictTag :value="row.verdict" /></template></el-table-column
><el-table-column label="语言" prop="language" width="90" /><el-table-column
v-if="!compact"
label="用时"
width="90"
align="right"
><template #default="{ row }">{{
row.time === null ? '—' : row.time + ' ms'
}}</template></el-table-column
><el-table-column v-if="!compact" label="内存" width="100" align="right"
><template #default="{ row }">{{
row.memory === null ? '—' : row.memory + ' KiB'
}}</template></el-table-column
><el-table-column label="提交时间" prop="created" min-width="175"
/></el-table>
</template>
+270
View File
@@ -0,0 +1,270 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { TagCanvas } from '@misskey-dev/tagcanvas-es'
const props = defineProps<{ tags: readonly string[] }>()
const emit = defineEmits<{ select: [tag: string] }>()
const stage = ref<HTMLDivElement>()
const canvas = ref<HTMLCanvasElement>()
const source = ref<HTMLDivElement>()
const width = ref(300)
const ready = ref(false)
const rotationEnabled = ref(true)
const interaction = ref<'follow' | 'drag'>('follow')
const visible = ref(false)
const hidden = ref(false)
const colors = ['#409eff', '#638ac5', '#588db8', '#7496b8']
const fontSize = computed(() => {
const textArea = props.tags.reduce((sum, tag) => sum + [...tag].length + 1.8, 0)
const radius = Math.max(35, (Math.min(width.value, 300) - 68) / 2)
return Math.max(
12,
Math.min(19, Math.sqrt((4 * Math.PI * radius ** 2 * 0.23) / Math.max(1, textArea * 1.35))),
)
})
function weight(tag: string, index: number) {
const emphasis = ['动态规划', '图论'].includes(tag) ? 1.2 : index % 3 === 0 ? 1.08 : 1
return emphasis * Math.min(1, Math.sqrt(4 / Math.max(1, [...tag].length)))
}
let cloud: TagCanvas | undefined
let resizeObserver: ResizeObserver | undefined
let intersectionObserver: IntersectionObserver | undefined
let motionQuery: MediaQueryList | undefined
function syncAnimation() {
if (!visible.value || hidden.value) cloud?.pause()
else cloud?.resume()
}
function rebuild() {
cloud?.destroy()
cloud = undefined
ready.value = false
if (!canvas.value || !stage.value || !source.value || !props.tags.length) return
const ratio = Math.min(2, window.devicePixelRatio || 1)
canvas.value.width = Math.round(width.value * ratio)
canvas.value.height = 300 * ratio
try {
cloud = new TagCanvas(canvas.value, {
tagContainer: source.value,
hideTags: false,
shape: 'sphere',
textColor: null,
textFont: getComputedStyle(stage.value).fontFamily,
textHeight: fontSize.value * ratio,
weight: props.tags.length > 1,
weightFrom: 'data-weight',
weightMode: 'size',
weightSizeMin: (fontSize.value - 1) * ratio,
weightSizeMax: (fontSize.value + 2) * ratio,
bgColor: '#f0f7ff',
bgOutline: '#cfe2f7',
bgOutlineThickness: ratio,
bgRadius: 14 * ratio,
padding: 5 * ratio,
outlineColor: '#409eff',
outlineThickness: ratio,
outlineRadius: 14 * ratio,
outlineOffset: 1 * ratio,
outlineMethod: 'outline',
minBrightness: 0.45,
maxBrightness: 1,
depth: 0.55,
zoom: 0.98,
maxSpeed: rotationEnabled.value ? 0.02 : 0,
initial: rotationEnabled.value ? [0.16, -0.08] : [0, 0],
decel: 0.94,
dragControl: interaction.value === 'drag',
dragThreshold: 4,
freezeActive: true,
freezeDecel: true,
wheelZoom: false,
noTagsMessage: false,
activeAudio: false,
txtScale: 2,
})
ready.value = true
syncAnimation()
} catch {
// Keep the ordinary links available when canvas rendering is unavailable.
ready.value = false
}
}
function updateVisibility() {
hidden.value = document.hidden
}
function updateMotionPreference() {
if (motionQuery?.matches) rotationEnabled.value = false
}
watch([() => props.tags, width, rotationEnabled, interaction], rebuild, {
deep: true,
flush: 'post',
})
watch([visible, hidden], syncAnimation)
onMounted(() => {
if (!stage.value) return
width.value = stage.value.getBoundingClientRect().width
motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
rotationEnabled.value = !motionQuery.matches
if (window.matchMedia('(pointer: coarse)').matches) interaction.value = 'drag'
motionQuery.addEventListener('change', updateMotionPreference)
updateVisibility()
document.addEventListener('visibilitychange', updateVisibility)
resizeObserver = new ResizeObserver(([entry]) => {
if (entry) width.value = entry.contentRect.width
})
resizeObserver.observe(stage.value)
intersectionObserver = new IntersectionObserver(([entry]) => {
visible.value = !!entry?.isIntersecting
})
intersectionObserver.observe(stage.value)
rebuild()
})
onBeforeUnmount(() => {
cloud?.destroy()
resizeObserver?.disconnect()
intersectionObserver?.disconnect()
motionQuery?.removeEventListener('change', updateMotionPreference)
document.removeEventListener('visibilitychange', updateVisibility)
})
</script>
<template>
<div class="tag-sphere">
<div ref="stage" class="tag-sphere-stage" role="group" aria-label="球形题目标签">
<canvas
v-show="ready"
ref="canvas"
class="sphere-canvas"
:class="{ draggable: interaction === 'drag' }"
role="img"
:aria-label="`球形标签云,${interaction === 'drag' ? '拖动旋转' : '跟随鼠标旋转'},点击标签筛选题目。键盘可选择下方标签链接。`"
/>
<div
ref="source"
class="sphere-source"
:class="{ 'source-hidden': ready }"
aria-label="题目标签"
>
<a
v-for="(tag, index) in tags"
:key="tag"
:href="'/problems?tag=' + encodeURIComponent(tag)"
:style="{ color: colors[index % colors.length] }"
:data-weight="weight(tag, index)"
@click.prevent="emit('select', tag)"
>{{ tag }}</a
>
</div>
<p v-if="!tags.length" class="sphere-empty">暂无题目标签</p>
</div>
<div v-if="ready" class="sphere-controls">
<span>点击标签查看题目</span>
<div class="sphere-control-buttons">
<button
:aria-pressed="interaction === 'drag'"
aria-label="切换标签球交互方式"
@click="interaction = interaction === 'follow' ? 'drag' : 'follow'"
>
{{ interaction === 'follow' ? '鼠标跟随' : '拖动旋转' }}
</button>
<button :aria-pressed="rotationEnabled" @click="rotationEnabled = !rotationEnabled">
{{ rotationEnabled ? '暂停旋转' : '开始旋转' }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.tag-sphere-stage {
position: relative;
width: 100%;
height: 300px;
}
.sphere-canvas {
width: 100%;
height: 300px;
display: block;
touch-action: none;
user-select: none;
}
.sphere-canvas.draggable {
cursor: grab;
}
.sphere-canvas.draggable:active {
cursor: grabbing;
}
.sphere-source {
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
height: 100%;
padding: 20px;
}
.sphere-source a {
padding: 5px 10px;
background: #f0f7ff;
border: 1px solid #dceaf9;
border-radius: 999px;
font-size: 14px;
}
.sphere-source.source-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.sphere-source.source-hidden:focus-within {
inset: 0;
width: 100%;
height: 100%;
padding: 20px;
clip-path: none;
overflow: auto;
background: #fff;
}
.sphere-source a:focus-visible {
outline: 2px solid #409eff;
outline-offset: 2px;
}
.sphere-empty {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #99aabd;
font-size: 14px;
}
.sphere-controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
color: #99aabd;
font-size: 11px;
padding-top: 4px;
}
.sphere-control-buttons {
display: flex;
gap: 6px;
}
.sphere-controls button {
color: #629edb;
background: #f5faff;
border: 1px solid #e6f0fb;
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
}
.sphere-controls button:focus-visible {
outline: 2px solid #90c2fb;
outline-offset: 2px;
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { computed } from 'vue'
import {
House,
Grid,
Collection,
Trophy,
Tickets,
Histogram,
ChatDotRound,
UserFilled,
InfoFilled,
Setting,
ArrowRight,
Refresh,
Search,
Monitor,
Bell,
ArrowDown,
Reading,
Document,
Clock,
Connection,
FolderOpened,
} from '@element-plus/icons-vue'
const props = defineProps<{ name: string }>()
const icons = {
home: House,
problems: Grid,
trainings: Collection,
contests: Trophy,
submissions: Tickets,
rankings: Histogram,
discussions: ChatDotRound,
teams: UserFilled,
about: InfoFilled,
settings: Setting,
arrow: ArrowRight,
refresh: Refresh,
search: Search,
monitor: Monitor,
bell: Bell,
down: ArrowDown,
reading: Reading,
document: Document,
clock: Clock,
connection: Connection,
folder: FolderOpened,
}
const icon = computed(() => icons[props.name as keyof typeof icons] ?? Document)
</script>
<template>
<el-icon aria-hidden="true"><component :is="icon" /></el-icon>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { verdictLabels, type Verdict } from '../data/preview'
defineProps<{ value: Verdict; full?: boolean }>()
</script>
<template>
<span class="verdict" :class="`verdict-${value.toLowerCase()}`">{{
full ? verdictLabels[value] : value === 'Pending' ? '等待评测' : value
}}</span>
</template>
+88
View File
@@ -0,0 +1,88 @@
import { computed, onScopeDispose, ref, watch } from 'vue'
export type Language = 'cpp' | 'python' | 'rust'
export const languageLabels: Record<Language, string> = {
cpp: 'C++',
python: 'Python',
rust: 'Rust',
}
export const codeTemplates: Record<Language, string> = {
cpp: '#include <iostream>\nusing namespace std;\n\nint main() {\n // 在这里编写你的解法\n return 0;\n}\n',
python: '# 在这里编写你的解法\n\n',
rust: 'use std::io::{self, Read};\n\nfn main() {\n let mut input = String::new();\n io::stdin().read_to_string(&mut input).unwrap();\n // 在这里编写你的解法\n}\n',
}
export function useCodeDraft(problemId: string) {
const key = 'kf-oj:code-draft:' + problemId
const language = ref<Language>('cpp')
const drafts = ref({ ...codeTemplates })
const state = ref<'initial' | 'saved' | 'changed' | 'unavailable'>('initial')
let timer: ReturnType<typeof setTimeout> | undefined
try {
const raw = localStorage.getItem(key)
if (raw) {
const saved: unknown = JSON.parse(raw)
if (
saved &&
typeof saved === 'object' &&
'version' in saved &&
saved.version === 1 &&
'drafts' in saved &&
saved.drafts &&
typeof saved.drafts === 'object'
) {
const storedDrafts = saved.drafts as Record<string, unknown>
for (const lang of ['cpp', 'python', 'rust'] as const) {
const value = storedDrafts[lang]
if (typeof value === 'string') drafts.value[lang] = value
}
if ('language' in saved && ['cpp', 'python', 'rust'].includes(String(saved.language)))
language.value = saved.language as Language
state.value = 'saved'
}
}
} catch {
state.value = 'unavailable'
}
const code = computed({
get: () => drafts.value[language.value],
set: (value) => {
drafts.value[language.value] = value
},
})
function save() {
clearTimeout(timer)
try {
localStorage.setItem(
key,
JSON.stringify({ version: 1, language: language.value, drafts: drafts.value }),
)
state.value = 'saved'
} catch {
state.value = 'unavailable'
}
}
watch(
[drafts, language],
() => {
state.value = 'changed'
clearTimeout(timer)
timer = setTimeout(save, 400)
},
// Mark pending changes before a lifecycle event can run in the same tick.
{ deep: true, flush: 'sync' },
)
function flushPendingDraft() {
if (state.value === 'changed') save()
}
function onVisibilityChange() {
if (document.visibilityState === 'hidden') flushPendingDraft()
}
window.addEventListener('pagehide', flushPendingDraft)
document.addEventListener('visibilitychange', onVisibilityChange)
onScopeDispose(() => {
flushPendingDraft()
clearTimeout(timer)
window.removeEventListener('pagehide', flushPendingDraft)
document.removeEventListener('visibilitychange', onVisibilityChange)
})
return { language, code, state, save }
}
+13
View File
@@ -0,0 +1,13 @@
import { computed, ref, watch, type Ref } from 'vue'
export function usePagination<T>(items: Readonly<Ref<T[]>>, initialSize = 10) {
const page = ref(1)
const pageSize = ref(initialSize)
const total = computed(() => items.value.length)
const paged = computed(() =>
items.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value),
)
watch([items, pageSize], () => {
page.value = 1
})
return { page, pageSize, total, paged }
}
@@ -0,0 +1,28 @@
import { onMounted, onScopeDispose, ref } from 'vue'
import { fetchLiveness } from '../api/health'
export function usePlatformHealth() {
const state = ref<'loading' | 'connected' | 'unavailable'>('loading')
const version = ref('')
let controller: AbortController | undefined
async function refresh() {
controller?.abort()
const request = new AbortController()
controller = request
state.value = 'loading'
try {
const health = await fetchLiveness(request.signal)
if (!request.signal.aborted) {
version.value = health.version
state.value = 'connected'
}
} catch {
if (!request.signal.aborted) state.value = 'unavailable'
}
}
onMounted(refresh)
onScopeDispose(() => controller?.abort())
return { state, version, refresh }
}
+465
View File
@@ -0,0 +1,465 @@
// Explicit UI fixtures; business endpoints will be connected through API composables.
export type Difficulty = '入门' | '简单' | '中等' | '困难'
export interface Problem {
id: string
title: string
difficulty: Difficulty
tags: string[]
accepted: number
submissions: number
updated: string
description: string
input: string
output: string
sampleInput: string
sampleOutput: string
constraint: string
}
const definitions: [string, string, Difficulty, string[], number, number][] = [
['1001', 'A + B Problem', '入门', ['模拟', '输入输出'], 428, 536],
['1002', '两数之和', '简单', ['数组', '哈希表'], 315, 482],
['1003', '最长无重复子串', '中等', ['字符串', '滑动窗口'], 206, 527],
['1004', '旅行者的背包', '中等', ['动态规划', '背包'], 186, 461],
['1005', '城市间的最短路径', '中等', ['图论', '最短路'], 152, 387],
['1006', '寻找第一个位置', '简单', ['二分', '数组'], 241, 402],
['1007', '括号匹配', '简单', ['栈', '字符串'], 287, 389],
['1008', '岛屿的数量', '中等', ['搜索', '图论'], 143, 328],
['1009', '区间求和', '中等', ['数据结构', '前缀和'], 124, 306],
['1010', '最小生成树', '困难', ['图论', '并查集'], 96, 288],
['1011', '活动安排', '简单', ['贪心', '排序'], 203, 312],
['1012', '最长公共子序列', '中等', ['动态规划', '字符串'], 112, 345],
]
export const problems: Problem[] = definitions.map(
([id, title, difficulty, tags, accepted, submissions], index) => ({
id,
title,
difficulty,
tags,
accepted,
submissions,
updated: '2026-10-' + String(4 - Math.floor(index / 4)).padStart(2, '0'),
description:
id === '1001'
? '给定两个整数 A 和 B,请计算它们的和。认真阅读输入和输出格式,完成你的第一次提交。'
: '这道题的完整题面将在题库接入后展示。当前提供列表与编辑器的界面预览。',
input:
id === '1001'
? '输入一行,包含两个整数 A 和 B,以一个空格分隔。'
: '完整输入格式待题库接入。',
output: id === '1001' ? '输出一个整数,表示 A 与 B 的和。' : '完整输出格式待题库接入。',
sampleInput: id === '1001' ? '1 2' : '',
sampleOutput: id === '1001' ? '3' : '',
constraint:
id === '1001'
? '−10⁹ ≤ A, B ≤ 10⁹。建议使用 64 位整数保存计算结果。'
: '数据范围待题库接入。',
}),
)
export const problemTags = [...new Set(problems.flatMap((problem) => problem.tags))]
export const rankings = [
{
username: 'Ada',
motto: '每天进步一点点',
solved: 167,
submissions: 238,
rating: 1924,
color: '#6196ec',
},
{
username: 'Kronecker',
motto: '写下思路,再写代码。',
solved: 142,
submissions: 216,
rating: 1846,
color: '#6b70d6',
},
{
username: 'Sora',
motto: '今天也要 AC',
solved: 129,
submissions: 204,
rating: 1732,
color: '#3fb7a1',
},
{
username: 'Natsume',
motto: '慢慢来,比较快',
solved: 118,
submissions: 182,
rating: 1685,
color: '#e9a35d',
},
{
username: 'Lin',
motto: '保持好奇',
solved: 106,
submissions: 175,
rating: 1621,
color: '#dc82a4',
},
{
username: 'Mika',
motto: '从一道题开始',
solved: 98,
submissions: 153,
rating: 1594,
color: '#63a9c9',
},
{
username: 'Chen',
motto: 'Hello, algorithm',
solved: 92,
submissions: 151,
rating: 1540,
color: '#897cb7',
},
{
username: 'Yuki',
motto: '专注这一题',
solved: 87,
submissions: 144,
rating: 1506,
color: '#6aaa8a',
},
{
username: 'Joago',
motto: 'Thinking in code',
solved: 81,
submissions: 132,
rating: 1482,
color: '#bb9b67',
},
{
username: 'Hyperbola',
motto: '努力成为更好的自己',
solved: 74,
submissions: 128,
rating: 1428,
color: '#8095b9',
},
]
export const notices = [
{
id: '1',
title: '欢迎来到 KF OnlineJudge',
date: '2026-10-04',
category: '平台公告',
text: '从基础练习开始,记录每一次提交,让思路在代码里落地。当前页面使用示例内容。',
},
{
id: '2',
title: '新学期基础训练题单已更新',
date: '2026-10-03',
category: '训练',
text: '输入输出、数组、字符串和简单搜索,按顺序完成你的第一组算法练习。',
},
{
id: '3',
title: '周末练习赛 · 热身题单',
date: '2026-10-01',
category: '比赛',
text: '练习赛页面已准备好,实际比赛安排以平台发布的信息为准。',
},
]
export const trainings = [
{
id: 'basics',
title: '从零开始 · 编程基础',
category: '基础入门',
level: '入门',
description: '从输入输出到数组与字符串,熟悉在线评测的基本流程。',
ids: ['1001', '1002', '1006', '1007'],
color: 'blue',
icon: '01',
},
{
id: 'search',
title: '搜索与图论入门',
category: '算法专题',
level: '中等',
description: '理解遍历、连通性与路径,建立图论的基础解题思路。',
ids: ['1008', '1005', '1010'],
color: 'green',
icon: '02',
},
{
id: 'dp',
title: '动态规划 · 一步一步推出来',
category: '算法专题',
level: '中等',
description: '从状态定义开始,练习转移、边界和空间优化。',
ids: ['1004', '1012'],
color: 'violet',
icon: '03',
},
{
id: 'structures',
title: '常用数据结构练习',
category: '进阶练习',
level: '中等',
description: '通过具体问题理解哈希表、栈、前缀和与并查集。',
ids: ['1002', '1007', '1009', '1010'],
color: 'amber',
icon: '04',
},
]
export const contests = [
{
id: 'weekly-01',
title: 'KF 周末练习赛 #01',
type: 'ACM',
status: '即将开始',
start: '2026-10-10 14:00',
duration: '2 小时',
description: '五道由浅入深的算法题,给自己一个专注思考的下午。',
ids: ['1001', '1002', '1003', '1004', '1005'],
},
{
id: 'starter',
title: '新同学编程热身赛',
type: 'OI',
status: '即将开始',
start: '2026-10-12 19:00',
duration: '2 小时',
description: '面向刚开始学习程序设计的同学,练习输入输出与基本逻辑。',
ids: ['1001', '1006', '1007'],
},
{
id: 'graph',
title: '图论专题 · 路径与连通性',
type: 'ACM',
status: '进行中',
start: '2026-10-04 14:00',
duration: '4 小时',
description: '题单式比赛界面示例,状态与时间为展示用数据。',
ids: ['1005', '1008', '1010'],
},
{
id: 'weekly-00',
title: 'KF 周末练习赛 #00',
type: 'ACM',
status: '已结束',
start: '2026-09-27 14:00',
duration: '2 小时',
description: '往期比赛界面示例,便于浏览题目和比赛说明。',
ids: ['1001', '1003', '1011'],
},
{
id: 'dp-practice',
title: '动态规划专项练习',
type: 'OI',
status: '已结束',
start: '2026-09-20 09:00',
duration: '3 小时',
description: '集中练习常见动态规划模型。',
ids: ['1004', '1012'],
},
]
export type Verdict = 'AC' | 'WA' | 'TLE' | 'RE' | 'CE' | 'Pending'
export const verdictLabels: Record<Verdict, string> = {
AC: 'Accepted',
WA: 'Wrong Answer',
TLE: 'Time Limit Exceeded',
RE: 'Runtime Error',
CE: 'Compile Error',
Pending: '等待评测',
}
export const submissions: {
id: string
problemId: string
username: string
verdict: Verdict
language: string
time: number | null
memory: number | null
created: string
}[] = [
{
id: '10248',
problemId: '1001',
username: 'Kronecker',
verdict: 'AC',
language: 'Rust',
time: 12,
memory: 2048,
created: '2026-10-04 14:32:18',
},
{
id: '10247',
problemId: '1003',
username: 'Ada',
verdict: 'AC',
language: 'C++',
time: 34,
memory: 4096,
created: '2026-10-04 14:30:05',
},
{
id: '10246',
problemId: '1004',
username: 'Sora',
verdict: 'WA',
language: 'Python',
time: 62,
memory: 8192,
created: '2026-10-04 14:28:41',
},
{
id: '10245',
problemId: '1005',
username: 'Lin',
verdict: 'TLE',
language: 'C++',
time: 1000,
memory: 6144,
created: '2026-10-04 14:26:22',
},
{
id: '10244',
problemId: '1002',
username: 'Mika',
verdict: 'AC',
language: 'Python',
time: 48,
memory: 7168,
created: '2026-10-04 14:24:09',
},
{
id: '10243',
problemId: '1007',
username: 'Chen',
verdict: 'CE',
language: 'C++',
time: null,
memory: null,
created: '2026-10-04 14:21:33',
},
{
id: '10242',
problemId: '1008',
username: 'Yuki',
verdict: 'RE',
language: 'Rust',
time: 18,
memory: 3072,
created: '2026-10-04 14:19:06',
},
{
id: '10241',
problemId: '1006',
username: 'Kronecker',
verdict: 'AC',
language: 'Rust',
time: 11,
memory: 2048,
created: '2026-10-04 14:17:26',
},
{
id: '10240',
problemId: '1011',
username: 'Natsume',
verdict: 'Pending',
language: 'C++',
time: null,
memory: null,
created: '2026-10-04 14:15:12',
},
{
id: '10239',
problemId: '1009',
username: 'Ada',
verdict: 'AC',
language: 'C++',
time: 22,
memory: 4096,
created: '2026-10-04 14:12:45',
},
{
id: '10238',
problemId: '1002',
username: 'Sora',
verdict: 'WA',
language: 'Python',
time: 56,
memory: 7168,
created: '2026-10-04 14:10:18',
},
{
id: '10237',
problemId: '1012',
username: 'Hyperbola',
verdict: 'AC',
language: 'C++',
time: 71,
memory: 10240,
created: '2026-10-04 14:08:51',
},
]
export const discussions = [
{
id: '1',
title: '第一次使用在线评测,可以从哪里开始?',
category: '交流',
author: 'Kronecker',
replies: 12,
views: 328,
date: '2026-10-04',
pinned: true,
content:
'欢迎来到 KF OnlineJudge!可以先打开「从零开始 · 编程基础」题单,了解标准输入输出,再尝试 A + B Problem。\n\n遇到问题时,记录题号、语言和具体报错,把自己的思路一起写出来,讨论会更容易展开。',
},
{
id: '2',
title: 'A + B Problem:输入输出与整数范围',
category: '题解',
author: 'Ada',
replies: 8,
views: 216,
date: '2026-10-04',
pinned: false,
content:
'读取两个整数,计算它们的和,输出结果。\n\n记得检查整数范围。Rust 可以选择 i64;Python 的整数可以直接参与计算。输出只需要答案和换行。',
problemId: '1001',
},
{
id: '3',
title: '动态规划中的状态应该怎么定义?',
category: '问答',
author: 'Sora',
replies: 6,
views: 184,
date: '2026-10-03',
pinned: false,
content:
'练习背包问题时,我经常卡在状态定义上。现在尝试先描述「已经处理哪些物品」和「剩余多少容量」,再考虑每一步有哪些选择。\n\n大家平时会怎样从题意里提取状态?',
},
{
id: '4',
title: '分享我的 Rust 刷题输入模板',
category: '交流',
author: 'Lin',
replies: 4,
views: 152,
date: '2026-10-02',
pinned: false,
content:
'先用 read_to_string 获取输入,再按空白分隔。\n\n不同题目的输入结构可能不同,使用模板之前还是需要认真阅读题面。',
},
{
id: '5',
title: '关于题库筛选的一点建议',
category: '反馈',
author: 'Mika',
replies: 2,
views: 96,
date: '2026-10-01',
pinned: false,
content: '希望题库可以同时按照标签和难度筛选,这样练习某个知识点会更方便。',
},
]
export function findProblem(id: string) {
return problems.find((problem) => problem.id === id)
}
export function acceptance(problem: Pick<Problem, 'accepted' | 'submissions'>) {
return Math.round((problem.accepted / problem.submissions) * 100)
}

Some files were not shown because too many files have changed in this diff Show More