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:
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),构建仍有包体告警,后续优化拆包和加载。 - 业务写入、认证和实际判题能力仍处于工程骨架阶段;沙箱继续拒绝未经验证隔离的执行。
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.zip binary
|
||||
+35
@@ -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
|
||||
@@ -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`,记录实际通过项和未验证的能力。
|
||||
沙箱的权限、隔离、超限和破坏性测试单独在规定执行环境验证。
|
||||
@@ -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
|
||||
@@ -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)。引用或复用其他项目代码时记录来源并遵守对应许可证。
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
3.13.16
|
||||
@@ -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` 并设置实际数据库连接。
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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"}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
"""KF-OnlineJudge platform, application modules and learning workers."""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
@@ -0,0 +1 @@
|
||||
"""HTTP transport and application composition."""
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""Platform API version 1."""
|
||||
@@ -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__)
|
||||
@@ -0,0 +1 @@
|
||||
"""Versioned transport models; never import business or infrastructure code."""
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""Cross-cutting configuration and logging; no business or transport dependencies."""
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Database primitives; all schema migrations live in backend/migrations."""
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
@@ -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."""
|
||||
@@ -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)."""
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""Independent process composition roots; task loops are implemented per milestone."""
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from kf_oj.workers.bootstrap import check_entrypoint
|
||||
|
||||
|
||||
def main() -> None:
|
||||
check_entrypoint("learning-worker")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
from kf_oj.workers.bootstrap import check_entrypoint
|
||||
|
||||
|
||||
def main() -> None:
|
||||
check_entrypoint("outbox-publisher")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
@@ -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())
|
||||
Generated
+677
@@ -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" },
|
||||
]
|
||||
@@ -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 组合层;运行期的契约模型禁止依赖业务模块。
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)。
|
||||
@@ -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:
|
||||
@@ -0,0 +1,7 @@
|
||||
# 可复现实验
|
||||
|
||||
后续负载生成、调度对照、故障注入和统计脚本放在本目录;工程骨架阶段未开始性能实验。
|
||||
每次实验保存 Git 提交、环境、配置、负载摘要、原始事件、结果和复现命令。
|
||||
|
||||
`results/` 默认忽略,报告只提交去标识摘要和原始材料的复现索引。
|
||||
每项实验先明确条件、正确性检查和验收指标,再运行并保留复现材料。
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
26.10.0
|
||||
@@ -0,0 +1,2 @@
|
||||
# pnpm 11 reads project settings from pnpm-workspace.yaml.
|
||||
# Keep this file for future registry/auth configuration only.
|
||||
@@ -0,0 +1,4 @@
|
||||
pnpm-lock.yaml
|
||||
dist
|
||||
node_modules
|
||||
src/api/generated
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -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`。
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+2398
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
saveExact: true
|
||||
verifyDepsBeforeRun: false
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -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 |
@@ -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 >> a >> b;</text><text x="106" y="151" font-family="monospace" font-size="14" fill="#455e80">cout << 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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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' },
|
||||
]
|
||||
@@ -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 管理控制台`
|
||||
})
|
||||
@@ -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
@@ -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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user