diff --git a/.env.example b/.env.example index 12cf52b0..014b843a 100644 --- a/.env.example +++ b/.env.example @@ -35,11 +35,21 @@ DATA_SERVICE_URL=http://localhost:8001 PAPER_SERVICE_URL=http://localhost:8002 RESEARCH_SERVICE_URL=http://localhost:8003 FACTOR_SERVICE_URL=http://localhost:8004 +EVOLVER_SERVICE_URL=http://localhost:8005 DATA_SERVICE_PORT=8001 PAPER_SERVICE_PORT=8002 RESEARCH_SERVICE_PORT=8003 FACTOR_SERVICE_PORT=8004 +EVOLVER_SERVICE_PORT=8005 +# 可选 self-host capability gate;false 时 Dashboard 明确返回“演化服务未启用”。 +EVOLVER_ENABLED=true + +# Evolver E1 单 worker / 单副本;每个 run 只跑一代候选。 +EVOLVER_MAX_RUNNING_RUNS=1 +EVOLVER_ACCOUNT_ACTIVE_LIMIT=2 +EVOLVER_JOB_TIMEOUT_S=300 +EVOLVER_JOB_MEM_GB=2 # factor 服务可选项(ADR-0043):qlib Alpha158 风格因子纯 pandas 本地算,默认开; # 设 false 可整源关闭。snapshot top-N 去相关阈值默认 0.85(1.0 = 关闭去相关) diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index dd3ee6ef..775a1350 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -1,8 +1,8 @@ name: Build & Push Images -# CI 构建 6 个生产镜像(data/paper/research/factor + mastra + dashboard)并推送 GHCR。 +# CI 构建 7 个生产镜像(data/paper/research/factor/evolver + mastra + dashboard)并推送 GHCR。 # VPS 端只 `docker compose pull` 不 `build`,绕开小内存机现场 build OOM。 -# 见 docs/miro/decisions/0058。部署仍是手动 `scripts/deploy.sh`(非自动 CD)。 +# 部署仍是手动 `scripts/deploy.sh`(非自动 CD)。 # # 镜像命名:ghcr.io//inalpha-:{latest,} # migrate 服务复用 inalpha-paper 镜像(不单独构建)。 @@ -69,6 +69,33 @@ jobs: cache-from: type=gha,scope=${{ matrix.service }} cache-to: type=gha,mode=max,scope=${{ matrix.service }} + evolver: + name: build · evolver + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Resolve owner (lowercase) + id: meta + run: echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" + - uses: docker/setup-buildx-action@v3 + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push + uses: docker/build-push-action@v6 + with: + context: services + file: infra/docker/Dockerfile.evolver + push: true + tags: | + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/inalpha-evolver:latest + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/inalpha-evolver:${{ github.sha }} + cache-from: type=gha,scope=evolver + cache-to: type=gha,mode=max,scope=evolver + mastra: name: build · mastra runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87e91d8c..421d8c84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,7 +171,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - service: [data, paper, research, factor] + service: [data, paper, research, factor, evolver] steps: - uses: actions/checkout@v4 - name: Install uv @@ -189,3 +189,58 @@ jobs: - name: Mypy (best-effort, allow failure) working-directory: services/${{ matrix.service }} run: uv run mypy . || true + + python-e1-tests: + name: E1 · ${{ matrix.service }} pytest + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + service: [paper, evolver] + services: + timescaledb: + image: timescale/timescaledb:2.27.2-pg17 + env: + POSTGRES_USER: quant + POSTGRES_PASSWORD: devpass + POSTGRES_DB: inalpha_migration_ci_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U quant -d inalpha_migration_ci_test" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + env: + DATABASE_URL: postgresql+psycopg://quant:devpass@localhost:5432/inalpha_test + INALPHA_MIGRATION_TEST_DATABASE_URL: postgresql+psycopg://quant:devpass@localhost:5432/inalpha_migration_ci_test + EVOLVER_TEST_DATABASE_URL: postgresql+psycopg://quant:devpass@localhost:5432/inalpha_migration_ci_test + JWT_SECRET: test-secret-do-not-use-in-prod-please-and-thank-you + DATA_SERVICE_URL: http://data-mock.test + PAPER_POOL_DISABLED: "1" + INALPHA_LIVE_RUNNER_RESUME_ON_STARTUP: "false" + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - name: Set up Python + run: uv python install 3.12 + - name: Sync dependencies + working-directory: services/${{ matrix.service }} + run: uv sync --frozen + - name: Apply schema for Evolver tests + if: matrix.service == 'evolver' + working-directory: infra/migrations + run: uv run --project ../../services/evolver alembic upgrade head + env: + DATABASE_URL: postgresql+psycopg://quant:devpass@localhost:5432/inalpha_migration_ci_test + - name: Run service tests + working-directory: services/${{ matrix.service }} + run: uv run pytest -q tests + - name: Verify migration round trip + if: matrix.service == 'evolver' + working-directory: services/evolver + run: uv run pytest -q ../../infra/migrations/tests/test_migration_0038.py diff --git a/apps/dashboard/messages/en.json b/apps/dashboard/messages/en.json index fa9ce046..8b098784 100644 --- a/apps/dashboard/messages/en.json +++ b/apps/dashboard/messages/en.json @@ -497,6 +497,9 @@ "risk": "Risk panel", "activity": "Agent log", "divination": "Divination", + "evolution_list": "Strategy evolution", + "evolution_run_detail": "Evolution run", + "evolution_candidate_detail": "Evolution candidate", "overview": "Overview" } } @@ -540,8 +543,8 @@ "truncated": "Showing the latest {n} — earlier ones aren't listed here.", "filter": { "all": "All", - "done": "Done", - "running": "Running" + "terminal": "Finished", + "active": "Active" }, "col": { "run": "Run", @@ -552,7 +555,50 @@ "rejected": "Blocked", "cost": "Cost", "time": "Time" - } + }, + "detail": { + "back": "Back to evolution", + "title": "Evolution Run", + "abort": "Abort run", + "abortTitle": "Abort this evolution run?", + "abortDescription": "The current stage will be cancelled and no further slots will run. LLM costs already incurred cannot be reversed.", + "cancel": "Cancel", + "confirmAbort": "Abort run", + "aborting": "Aborting…", + "candidates": "Candidate slots", + "summary": "Run summary", + "stage": "Stage", + "attempts": "Attempts", + "succeeded": "Succeeded", + "rejected": "Blocked", + "cost": "Cost", + "failure": "Failure code", + "dataset": "Frozen dataset", + "seedReport": "Seed report", + "baseline": "Market baseline", + "notAvailable": "Not available yet", + "noCandidates": "No slots have been created yet.", + "slot": "Slot", + "outcome": "Outcome", + "fitness": "Fitness" + }, + "candidate": { + "back": "Back to evolution run", + "title": "Candidate #{slot}", + "error": "Rejection / failure", + "stage": "Stage", + "fitness": "Fitness", + "risk": "Overfitting risk", + "cost": "Cost", + "diff": "Unified diff", + "source": "Strategy source", + "evaluation": "Evaluation snapshot", + "copy": "Copy", + "copied": "Copied", + "notAvailable": "Not available yet" + }, + "loadMore": "Load more", + "loadingMore": "Loading…" }, "backtests": { "back": "Agent Activity", diff --git a/apps/dashboard/messages/zh.json b/apps/dashboard/messages/zh.json index 12f3c1a1..3e986f1a 100644 --- a/apps/dashboard/messages/zh.json +++ b/apps/dashboard/messages/zh.json @@ -497,6 +497,9 @@ "risk": "风控面板", "activity": "Agent 日志", "divination": "占卜台", + "evolution_list": "策略演化", + "evolution_run_detail": "演化运行", + "evolution_candidate_detail": "演化候选", "overview": "总览" } } @@ -540,8 +543,8 @@ "truncated": "仅展示最近 {n} 条,更早的记录未列出。", "filter": { "all": "全部", - "done": "已完成", - "running": "运行中" + "terminal": "已结束", + "active": "进行中" }, "col": { "run": "运行", @@ -552,7 +555,50 @@ "rejected": "阻断", "cost": "费用", "time": "时间" - } + }, + "detail": { + "back": "返回演化列表", + "title": "演化运行", + "abort": "中止运行", + "abortTitle": "确认中止演化?", + "abortDescription": "当前阶段会被取消,后续 slot 不再执行。已经发生的 LLM 费用不会撤销。", + "cancel": "取消", + "confirmAbort": "确认中止", + "aborting": "正在中止…", + "candidates": "候选 slot", + "summary": "运行摘要", + "stage": "阶段", + "attempts": "尝试", + "succeeded": "成功", + "rejected": "阻断", + "cost": "费用", + "failure": "失败码", + "dataset": "冻结数据集", + "seedReport": "种子报告", + "baseline": "市场基准", + "notAvailable": "尚不可用", + "noCandidates": "尚未产生 slot。", + "slot": "Slot", + "outcome": "结果", + "fitness": "适应度" + }, + "candidate": { + "back": "返回演化运行", + "title": "候选 #{slot}", + "error": "拒绝 / 失败原因", + "stage": "阶段", + "fitness": "适应度", + "risk": "过拟合风险", + "cost": "费用", + "diff": "统一 Diff", + "source": "策略源码", + "evaluation": "评估快照", + "copy": "复制", + "copied": "已复制", + "notAvailable": "尚不可用" + }, + "loadMore": "加载更多", + "loadingMore": "正在加载…" }, "backtests": { "back": "Agent 活动", diff --git a/apps/dashboard/src/app/[locale]/evolution/[runId]/page.tsx b/apps/dashboard/src/app/[locale]/evolution/[runId]/page.tsx new file mode 100644 index 00000000..59acd884 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/evolution/[runId]/page.tsx @@ -0,0 +1,14 @@ +import { setRequestLocale } from "next-intl/server"; + +import { EvolutionRunDetailClient } from "@/components/evolution/EvolutionRunDetailClient"; + +/** 单次 E1 演化运行及其 slot 结果。 */ +export default async function EvolutionRunPage({ + params, +}: { + params: Promise<{ locale: string; runId: string }>; +}) { + const { locale, runId } = await params; + setRequestLocale(locale); + return ; +} diff --git a/apps/dashboard/src/app/[locale]/evolution/candidates/[candidateId]/page.tsx b/apps/dashboard/src/app/[locale]/evolution/candidates/[candidateId]/page.tsx new file mode 100644 index 00000000..08d25816 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/evolution/candidates/[candidateId]/page.tsx @@ -0,0 +1,14 @@ +import { setRequestLocale } from "next-intl/server"; + +import { EvolutionCandidateDetailClient } from "@/components/evolution/EvolutionCandidateDetailClient"; + +/** 单个 E1 演化 slot 详情。 */ +export default async function EvolutionCandidatePage({ + params, +}: { + params: Promise<{ locale: string; candidateId: string }>; +}) { + const { locale, candidateId } = await params; + setRequestLocale(locale); + return ; +} diff --git a/apps/dashboard/src/app/api/activity/route.ts b/apps/dashboard/src/app/api/activity/route.ts index 82c29ca2..d03ca730 100644 --- a/apps/dashboard/src/app/api/activity/route.ts +++ b/apps/dashboard/src/app/api/activity/route.ts @@ -24,18 +24,6 @@ const MAX_RUNS_FOR_DECISIONS = 8; const ORDERS_LIMIT = 40; // ── 各数据源的原始响应(只声明用到的字段)── -interface SchedulerRunsResp { - runs: Array<{ - runId: string; - jobId: string; - scheduledAt: string; - startedAt: string; - finishedAt: string | null; - status: "running" | "success" | "failed" | "timeout"; - trigger: "cron" | "manual"; - error: unknown; - }>; -} interface SchedulerJobsResp { schedulerRunning: boolean; } @@ -81,9 +69,10 @@ interface RiskLocksResp { /** * GET /api/activity —— 跨模块 agent 活动流。 * - * 把 scheduler runs / 待审批 / 风控锁 / runner 生命周期与决策 / 订单 / 回测 归一成 ActivityEvent[], - * 按时间倒序合并。每个源独立 try —— 任一不可用(尤其 mastra:4111 可能没起)只标记 - * sources.=false,不拖垮整页;绝不把"取不到"静默当成"没有"。 + * 把 待审批 / 风控锁 / runner 生命周期与决策 / 订单 / 回测 / 会话 归一成 ActivityEvent[], + * 按时间倒序合并。系统 cron 运行记录属系统级、非账户事件,不进活动流(只读运行状态做 KPI)。 + * 每个源独立 try —— 任一不可用(尤其 mastra:4111 可能没起)只标记 sources.=false, + * 不拖垮整页;绝不把"取不到"静默当成"没有"。 */ export async function GET() { const sources = { @@ -104,10 +93,10 @@ export async function GET() { let pendingCount = 0; let activeLockCount = 0; - // mastra(scheduler / permissions):dev 端不需要 JWT,auth:false。 + // mastra scheduler 只读运行状态做 KPI(系统级,无 per-user 数据)保持 auth:false; + // permissions 需 JWT(走默认 auth)。 const [ jobsR, - runsR, pendingR, approvalHistoryR, locksR, @@ -120,18 +109,11 @@ export async function GET() { auth: false, timeoutMs: 5000, }), - backendFetch("mastra", "/scheduler/runs", { - auth: false, - query: { limit: 50 }, - timeoutMs: 5000, - }), backendFetch("mastra", "/permissions/pending", { - auth: false, timeoutMs: 5000, }), // 审批审计历史(终态)—— 决策/超时/重启扫尾后仍可回看,不再"决策即消失"。 backendFetch("mastra", "/permissions/history", { - auth: false, query: { limit: 20 }, timeoutMs: 5000, }), @@ -158,30 +140,9 @@ export async function GET() { listChatThreads(200, { backfillTitles: false }), ]); - // ── scheduler ── + // ── scheduler(仅运行状态 KPI;cron 运行记录属系统级,不进账户活动流)── if (jobsR.status === "fulfilled") schedulerRunning = jobsR.value.schedulerRunning; else sources.scheduler = false; - if (runsR.status === "fulfilled") { - for (const r of runsR.value.runs) { - events.push({ - id: `sched:${r.runId}`, - kind: "scheduler", - ts: r.finishedAt ?? r.startedAt ?? r.scheduledAt, - title: r.jobId, - detail: `${r.trigger} · ${r.status}`, - outcome: r.status, - tone: - r.status === "success" - ? "bull" - : r.status === "running" - ? "cyan" - : "fox", - href: null, - }); - } - } else { - sources.scheduler = false; - } // ── permissions(待审批)── if (pendingR.status === "fulfilled") { @@ -488,7 +449,6 @@ function orderTone(status: string): ActivityTone { function countByKind(events: ActivityEvent[]): Record { const c: Record = { - scheduler: 0, permission: 0, decision: 0, risk: 0, diff --git a/apps/dashboard/src/app/api/evolution/[runId]/route.ts b/apps/dashboard/src/app/api/evolution/[runId]/route.ts index e9a24985..1178f3a8 100644 --- a/apps/dashboard/src/app/api/evolution/[runId]/route.ts +++ b/apps/dashboard/src/app/api/evolution/[runId]/route.ts @@ -1,33 +1,63 @@ import { NextResponse } from "next/server"; -import { evolutionBackendFetch } from "@/lib/evolver-backend"; -import type { EvolutionRunDetailPayload } from "@/lib/types"; +import { backendFetch, BackendError } from "@/lib/backend"; +import { isEvolutionEnabled } from "@/lib/evolution-capability"; +import type { EvolutionRun, EvolutionRunDetailPayload } from "@/lib/types"; export const dynamic = "force-dynamic"; -/** - * GET /api/evolution/[runId] —— 演化运行详情(含候选完整列表)。 - * - * 调 evolver 服务的 GET /api/v1/runs/{run_id}。 - */ +function validUuid(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); +} + +function failure(error: unknown) { + const status = error instanceof BackendError ? error.status : 500; + return NextResponse.json( + { error: error instanceof Error ? error.message : "unknown error" }, + { status }, + ); +} + +function disabled() { + return NextResponse.json( + { error: "evolution service is not enabled", code: "EVOLUTION_SERVICE_DISABLED" }, + { status: 503 }, + ); +} + export async function GET( _request: Request, { params }: { params: Promise<{ runId: string }> }, ) { + if (!isEvolutionEnabled()) return disabled(); const { runId } = await params; + if (!validUuid(runId)) return NextResponse.json({ error: "invalid run id" }, { status: 400 }); try { - const data = await evolutionBackendFetch(`/api/v1/runs/${runId}`, { + const run = await backendFetch("evolver", `/api/v1/runs/${runId}`, { timeoutMs: 5000, }); - const payload: EvolutionRunDetailPayload = { - run: data as EvolutionRunDetailPayload["run"], - asOf: new Date().toISOString(), - }; - return NextResponse.json(payload, { - headers: { "Cache-Control": "no-store" }, + const payload: EvolutionRunDetailPayload = { run, asOf: new Date().toISOString() }; + return NextResponse.json(payload, { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + return failure(error); + } +} + +export async function POST( + _request: Request, + { params }: { params: Promise<{ runId: string }> }, +) { + if (!isEvolutionEnabled()) return disabled(); + const { runId } = await params; + if (!validUuid(runId)) return NextResponse.json({ error: "invalid run id" }, { status: 400 }); + try { + const run = await backendFetch("evolver", `/api/v1/runs/${runId}/abort`, { + method: "POST", + body: {}, + timeoutMs: 5000, }); - } catch (err) { - console.error("[evolution:run] fetch failed", err); - return NextResponse.json({ error: err instanceof Error ? err.message : "unknown error" }, { status: 500 }); + return NextResponse.json({ run, asOf: new Date().toISOString() }); + } catch (error) { + return failure(error); } -} \ No newline at end of file +} diff --git a/apps/dashboard/src/app/api/evolution/candidates/[candidateId]/route.ts b/apps/dashboard/src/app/api/evolution/candidates/[candidateId]/route.ts new file mode 100644 index 00000000..2a364a87 --- /dev/null +++ b/apps/dashboard/src/app/api/evolution/candidates/[candidateId]/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server"; + +import { backendFetch, BackendError } from "@/lib/backend"; +import { isEvolutionEnabled } from "@/lib/evolution-capability"; +import type { EvolutionCandidateDetailPayload, EvolutionCandidateSummary } from "@/lib/types"; + +export const dynamic = "force-dynamic"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ candidateId: string }> }, +) { + if (!isEvolutionEnabled()) { + return NextResponse.json( + { error: "evolution service is not enabled", code: "EVOLUTION_SERVICE_DISABLED" }, + { status: 503 }, + ); + } + const { candidateId } = await params; + const valid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(candidateId); + if (!valid) return NextResponse.json({ error: "invalid candidate id" }, { status: 400 }); + try { + const candidate = await backendFetch( + "evolver", + `/api/v1/candidates/${candidateId}`, + { timeoutMs: 5000 }, + ); + const payload: EvolutionCandidateDetailPayload = { + candidate, + asOf: new Date().toISOString(), + }; + return NextResponse.json(payload, { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + const status = error instanceof BackendError ? error.status : 500; + return NextResponse.json( + { error: error instanceof Error ? error.message : "unknown error" }, + { status }, + ); + } +} diff --git a/apps/dashboard/src/app/api/evolution/route.ts b/apps/dashboard/src/app/api/evolution/route.ts index 0fe5bef2..78443c2a 100644 --- a/apps/dashboard/src/app/api/evolution/route.ts +++ b/apps/dashboard/src/app/api/evolution/route.ts @@ -1,39 +1,38 @@ import { NextResponse } from "next/server"; -import { evolutionBackendFetch } from "@/lib/evolver-backend"; +import { backendFetch, BackendError } from "@/lib/backend"; +import { isEvolutionEnabled } from "@/lib/evolution-capability"; import type { EvolutionPayload, EvolutionRunSummary } from "@/lib/types"; export const dynamic = "force-dynamic"; -/** - * GET /api/evolution —— 演化运行列表(按 started_at 倒序)。 - * - * 调 evolver 服务的 GET /api/v1/runs(目前内存存储,evolver 重启后清空)。 - * 每个 run 附带候选摘要(按 fitness 降序,取前 3 条)。 - */ -export async function GET() { - try { - const RUNS_LIMIT = 50; - const raw = await evolutionBackendFetch( - "/api/v1/runs", - { query: { limit: RUNS_LIMIT + 1 }, timeoutMs: 5000 }, - ); - const truncated = raw.length > RUNS_LIMIT; - const runs = raw.slice(0, RUNS_LIMIT); +type RunsResponse = { items: EvolutionRunSummary[]; next_cursor: string | null }; +export async function GET(request: Request) { + if (!isEvolutionEnabled()) { + return NextResponse.json( + { error: "evolution service is not enabled", code: "EVOLUTION_SERVICE_DISABLED" }, + { status: 503 }, + ); + } + const { searchParams } = new URL(request.url); + const limit = Math.min(Number(searchParams.get("limit") ?? 50), 50); + try { + const raw = await backendFetch("evolver", "/api/v1/runs", { + query: { limit, cursor: searchParams.get("cursor") ?? undefined }, + timeoutMs: 5000, + }); const payload: EvolutionPayload = { - runs, - truncated, + runs: raw.items, + nextCursor: raw.next_cursor, asOf: new Date().toISOString(), }; - return NextResponse.json(payload, { - headers: { "Cache-Control": "no-store" }, - }); - } catch (err) { - console.error("[evolution] fetch failed", err); + return NextResponse.json(payload, { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + const status = error instanceof BackendError ? error.status : 500; return NextResponse.json( - { error: err instanceof Error ? err.message : "unknown error" }, - { status: 500 }, + { error: error instanceof Error ? error.message : "unknown error" }, + { status }, ); } -} \ No newline at end of file +} diff --git a/apps/dashboard/src/app/api/factors/candidates/route.ts b/apps/dashboard/src/app/api/factors/candidates/route.ts index 3534d53e..40c5066f 100644 --- a/apps/dashboard/src/app/api/factors/candidates/route.ts +++ b/apps/dashboard/src/app/api/factors/candidates/route.ts @@ -18,7 +18,7 @@ export async function GET(req: NextRequest) { const candidates = await backendFetch( "factor", `/candidates${qs}`, - { auth: false, timeoutMs: 6000 }, + { timeoutMs: 6000 }, ); return NextResponse.json({ available: true, candidates }); } catch { diff --git a/apps/dashboard/src/components/activity/ActivityClient.tsx b/apps/dashboard/src/components/activity/ActivityClient.tsx index b872da88..374168c4 100644 --- a/apps/dashboard/src/components/activity/ActivityClient.tsx +++ b/apps/dashboard/src/components/activity/ActivityClient.tsx @@ -18,7 +18,6 @@ import { ActivityFeed } from "./ActivityFeed"; const REFRESH_MS = 8000; const KINDS: ActivityKind[] = [ - "scheduler", "permission", "decision", "risk", diff --git a/apps/dashboard/src/components/activity/KindTag.tsx b/apps/dashboard/src/components/activity/KindTag.tsx index 807c0508..b7c28dd3 100644 --- a/apps/dashboard/src/components/activity/KindTag.tsx +++ b/apps/dashboard/src/components/activity/KindTag.tsx @@ -4,7 +4,6 @@ import { useTranslations } from "next-intl"; import { Activity, ArrowLeftRight, - Clock, FlaskConical, MessageSquare, Radio, @@ -17,7 +16,6 @@ import type { ActivityKind } from "@/lib/types"; import { cn } from "@/lib/cn"; const META: Record = { - scheduler: { icon: Clock, cls: "border-cyan/30 bg-cyan/10 text-cyan" }, permission: { icon: ShieldQuestion, cls: "border-gold/30 bg-gold/10 text-gold" }, decision: { icon: Activity, cls: "border-bull/30 bg-bull/10 text-bull" }, risk: { icon: ShieldAlert, cls: "border-fox-red/30 bg-fox-red/10 text-fox-red" }, diff --git a/apps/dashboard/src/components/evolution/EvolutionAbortDialog.tsx b/apps/dashboard/src/components/evolution/EvolutionAbortDialog.tsx new file mode 100644 index 00000000..5651b159 --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionAbortDialog.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + +/** 中止演化运行的 destructive 确认框。 */ +export function EvolutionAbortDialog({ + open, + busy, + onOpenChange, + onConfirm, +}: { + open: boolean; + busy: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; +}) { + const t = useTranslations("evolution.detail"); + return ( + + + + {t("abortTitle")} + {t("abortDescription")} + + + {t("cancel")} + { + event.preventDefault(); + onConfirm(); + }} + > + {busy ? t("aborting") : t("confirmAbort")} + + + + + ); +} diff --git a/apps/dashboard/src/components/evolution/EvolutionCandidateDetailClient.tsx b/apps/dashboard/src/components/evolution/EvolutionCandidateDetailClient.tsx new file mode 100644 index 00000000..fae4fa68 --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionCandidateDetailClient.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { ArrowLeft } from "lucide-react"; +import useSWR from "swr"; + +import { Link } from "@/i18n/navigation"; +import type { EvolutionCandidateDetailPayload } from "@/lib/types"; +import { jsonFetcher } from "@/lib/fetcher"; +import { CodeViewer } from "@/components/ui/CodeViewer"; +import { ErrorState, SkeletonBlock } from "@/components/ui/Feedback"; +import { LiveStrip } from "@/components/ui/LiveStrip"; +import { Panel } from "@/components/ui/Panel"; +import { StatusBadge } from "@/components/ui/StatusBadge"; + +/** 单个演化 slot 的源码、diff、评估和拒绝原因。 */ +export function EvolutionCandidateDetailClient({ candidateId }: { candidateId: string }) { + const t = useTranslations("evolution.candidate"); + const { data, error, isLoading, isValidating, mutate } = + useSWR(`/api/evolution/candidates/${candidateId}`, jsonFetcher, { + revalidateOnFocus: false, + }); + if (isLoading && !data) return
; + if (error && !data) return mutate()} />; + if (!data) return null; + const candidate = data.candidate; + return ( +
+ {t("back")} +

{t("title", { slot: candidate.slot + 1 })}

{candidate.candidate_id}

+ {(candidate.error_code || candidate.error_message) &&

{candidate.error_code ?? "EVOLUTION_SLOT_FAILED"}

{candidate.error_message ?? "—"}

} +
+ {candidate.unified_diff && } + {candidate.source_code && } + +
+ ); +} + +function Stat({ label, value }: { label: string; value: string }) { return

{label}

{value}

; } +function Snapshot({ title, value, empty }: { title: string; value: Record | null; empty: string }) { return {value ?
{JSON.stringify(value, null, 2)}
:

{empty}

}
; } diff --git a/apps/dashboard/src/components/evolution/EvolutionCandidates.tsx b/apps/dashboard/src/components/evolution/EvolutionCandidates.tsx new file mode 100644 index 00000000..a2e8630a --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionCandidates.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { Link } from "@/i18n/navigation"; +import type { EvolutionCandidateSummary } from "@/lib/types"; +import { StatusBadge } from "@/components/ui/StatusBadge"; + +/** 按 slot 展示一代候选及其阶段结果。 */ +export function EvolutionCandidates({ + candidates, +}: { + candidates: EvolutionCandidateSummary[]; +}) { + const t = useTranslations("evolution.detail"); + if (candidates.length === 0) { + return

{t("noCandidates")}

; + } + return ( +
+ + + + + + {candidates.map((candidate) => ( + + + + + + + + ))} + +
{t("slot")}{t("outcome")}{t("stage")}{t("fitness")}{t("cost")}
#{candidate.slot + 1}{candidate.stage}{candidate.fitness?.toFixed(4) ?? "—"}${(candidate.llm_cost_usd ?? 0).toFixed(4)}
+
+ ); +} + +function outcomeTone(outcome: string): "bull" | "fox" | "gold" | "muted" { + if (outcome === "succeeded") return "bull"; + if (outcome === "pending") return "gold"; + if (outcome === "cancelled") return "muted"; + return "fox"; +} diff --git a/apps/dashboard/src/components/evolution/EvolutionClient.tsx b/apps/dashboard/src/components/evolution/EvolutionClient.tsx index 01aa3508..a88a1f29 100644 --- a/apps/dashboard/src/components/evolution/EvolutionClient.tsx +++ b/apps/dashboard/src/components/evolution/EvolutionClient.tsx @@ -1,250 +1,41 @@ "use client"; -import { useMemo, useState } from "react"; -import { useLocale, useTranslations } from "next-intl"; -import { Workflow } from "lucide-react"; -import useSWR from "swr"; +import { useTranslations } from "next-intl"; -import type { EvolutionPayload } from "@/lib/types"; -import { cn } from "@/lib/cn"; -import { fmtNum, fmtRelative } from "@/lib/format"; -import { jsonFetcher } from "@/lib/fetcher"; +import { isEvolutionActive } from "@/lib/evolution"; +import { useEvolutionRuns } from "@/lib/use-evolution-runs"; import { ErrorState, SkeletonBlock } from "@/components/ui/Feedback"; import { LiveStrip } from "@/components/ui/LiveStrip"; import { PageHeader } from "@/components/ui/PageHeader"; -import { Panel } from "@/components/ui/Panel"; -import { StatusBadge } from "@/components/ui/StatusBadge"; -import { TruncationNote } from "@/components/ui/TruncationNote"; -import { Td, TableEmpty, TableHeadRow, Th } from "@/components/ui/Table"; - -/** 演化运行慢变,30s 一档。 */ -const REFRESH_MS = 30_000; - -type StatusFilter = "all" | "done" | "running"; -const FILTERS: StatusFilter[] = ["all", "done", "running"]; - -function runTone(status: string) { - if (status === "completed") return "bull"; - if (status === "running") return "cyan"; - if (status === "failed") return "fox"; - return "muted"; -} - -function runStatusLabel(status: string) { - if (status === "completed") return "DONE"; - if (status === "running") return "RUNNING"; - if (status === "failed") return "FAILED"; - return status.toUpperCase(); -} +import { EvolutionRunTable } from "./EvolutionRunTable"; +import { EvolutionStats } from "./EvolutionStats"; +/** E1 策略演化运行列表。 */ export function EvolutionClient() { const t = useTranslations("evolution"); - const locale = useLocale(); - const [filter, setFilter] = useState("all"); - - const { data, error, isValidating, isLoading, mutate } = useSWR( - "/api/evolution", - jsonFetcher, - { refreshInterval: REFRESH_MS, keepPreviousData: true }, - ); - - const rows = useMemo( - () => - data - ? filter === "all" - ? data.runs - : data.runs.filter( - (r) => r.status === (filter === "done" ? "completed" : "running"), - ) - : [], - [data, filter], - ); - - if (isLoading && !data) { - return ( -
- - -
- ); + const { + runs, + asOf, + error, + isValidating, + isLoading, + mutate, + hasMore, + isLoadingMore, + loadMore, + } = useEvolutionRuns(); + if (isLoading && runs.length === 0) { + return
; } - if (error && !data) { - return ( - mutate()} - /> - ); + if (error && runs.length === 0) { + return mutate()} />; } - if (!data) return null; - - const running = data.runs.filter((r) => r.status === "running").length; - const totalCost = data.runs.reduce((s, r) => s + r.llm_cost_usd, 0); - + const active = runs.filter((run) => isEvolutionActive(run.status)).length; return (
- - } - /> - - {/* KPI 条 */} -
- - 0 ? "cyan" : "muted"} - /> - - s + r.rejected_ast + r.rejected_contract + r.failed_eval, 0), - )} - tone="fox" - /> -
- - - {FILTERS.map((s) => ( - setFilter(s)} - /> - ))} -
- } - > - {rows.length === 0 ? ( - {t("empty")} - ) : ( -
- - - - - - - - - - - - - - - {rows.map((r) => ( - - ))} - -
{t("col.run")}{t("col.status")}{t("col.seed")}{t("col.budget")}{t("col.candidates")}{t("col.rejected")}{t("col.cost")}{t("col.time")}
-
- )} - - - {data.truncated && ( - - )} - - ); -} - -function Row({ - r, - locale, - t, -}: { - r: EvolutionPayload["runs"][number]; - locale: string; - t: ReturnType; -}) { - const nowMs = Date.now(); - const ago = fmtRelative(r.finished_at ?? r.started_at, nowMs, locale); - return ( - - - {r.run_id.slice(0, 8)} - - - - - - {r.seed_strategy_id} - - - {r.budget} - - - {r.candidates_count} - - - {r.rejected_ast + r.rejected_contract + r.failed_eval} - - - ${r.llm_cost_usd.toFixed(4)} - - - {ago} - - - ); -} - -function Stat({ - label, - value, - tone = "fg", -}: { - label: string; - value: string; - tone?: string; -}) { - return ( -
-
- {label} -
-
- {value} -
+ } /> + sum + run.llm_cost_usd, 0)} rejected={runs.reduce((sum, run) => sum + run.rejected, 0)} /> + void loadMore()} />
); } - -function FilterChip({ - label, - active, - onClick, -}: { - label: string; - active: boolean; - onClick: () => void; -}) { - return ( - - ); -} \ No newline at end of file diff --git a/apps/dashboard/src/components/evolution/EvolutionRunData.tsx b/apps/dashboard/src/components/evolution/EvolutionRunData.tsx new file mode 100644 index 00000000..910b4e4d --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionRunData.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import type { EvolutionRun } from "@/lib/types"; +import { Panel } from "@/components/ui/Panel"; + +/** 展示 seed、基准、冻结数据 manifest 与失败信息。 */ +export function EvolutionRunData({ run }: { run: EvolutionRun }) { + const t = useTranslations("evolution.detail"); + return ( +
+ +
+ + + + + + +
+ {run.failure_message && ( +

+ {run.failure_message} +

+ )} +
+ + + + + + + + + +
+ ); +} + +function Item({ label, value }: { label: string; value: string }) { + return
{label}
{value}
; +} + +function ObjectRows({ value, empty, preferred }: { value: Record | null; empty: string; preferred?: string[] }) { + if (!value) return

{empty}

; + const entries = preferred + ? preferred.filter((key) => key in value).map((key) => [key, value[key]] as const) + : Object.entries(value).filter(([, item]) => typeof item !== "object").slice(0, 12); + return
{entries.map(([key, item]) =>
{key}
{formatValue(item)}
)}
; +} + +function formatValue(value: unknown): string { + if (typeof value === "number") return Number.isInteger(value) ? String(value) : value.toFixed(4); + if (typeof value === "boolean") return value ? "true" : "false"; + return value == null ? "—" : String(value); +} diff --git a/apps/dashboard/src/components/evolution/EvolutionRunDetailClient.tsx b/apps/dashboard/src/components/evolution/EvolutionRunDetailClient.tsx new file mode 100644 index 00000000..24cb2567 --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionRunDetailClient.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { ArrowLeft } from "lucide-react"; +import useSWR from "swr"; + +import { Link } from "@/i18n/navigation"; +import type { EvolutionRunDetailPayload } from "@/lib/types"; +import { + evolutionRefreshInterval, +} from "@/lib/evolution"; +import { jsonFetcher } from "@/lib/fetcher"; +import { ErrorState, SkeletonBlock } from "@/components/ui/Feedback"; +import { Panel } from "@/components/ui/Panel"; +import { EvolutionAbortDialog } from "./EvolutionAbortDialog"; +import { EvolutionCandidates } from "./EvolutionCandidates"; +import { EvolutionRunData } from "./EvolutionRunData"; +import { EvolutionRunHeader } from "./EvolutionRunHeader"; + +/** 单次 E1 演化运行详情、条件轮询与中止操作。 */ +export function EvolutionRunDetailClient({ runId }: { runId: string }) { + const t = useTranslations("evolution.detail"); + const [confirmAbort, setConfirmAbort] = useState(false); + const [aborting, setAborting] = useState(false); + const [actionError, setActionError] = useState(null); + const { data, error, isLoading, isValidating, mutate } = + useSWR(`/api/evolution/${runId}`, jsonFetcher, { + refreshInterval: (latest) => evolutionRefreshInterval(latest?.run.status), + keepPreviousData: true, + }); + + const abort = async () => { + setAborting(true); + setActionError(null); + try { + const response = await fetch(`/api/evolution/${runId}`, { method: "POST" }); + const body = (await response.json()) as EvolutionRunDetailPayload & { error?: string }; + if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`); + await mutate(body, { revalidate: false }); + setConfirmAbort(false); + } catch (abortError) { + setActionError(abortError instanceof Error ? abortError.message : String(abortError)); + } finally { + setAborting(false); + } + }; + + if (isLoading && !data) return ; + if (error && !data) { + return mutate()} />; + } + if (!data) return null; + const { run } = data; + + return ( +
+ + + {t("back")} + + setConfirmAbort(true)} + /> + {actionError &&

{actionError}

} + + {run.attempted}/{run.budget}}> + + + void abort()} + /> +
+ ); +} + +function EvolutionDetailSkeleton() { + return
; +} diff --git a/apps/dashboard/src/components/evolution/EvolutionRunHeader.tsx b/apps/dashboard/src/components/evolution/EvolutionRunHeader.tsx new file mode 100644 index 00000000..ea7c03f8 --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionRunHeader.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import type { EvolutionRun } from "@/lib/types"; +import { evolutionTone, isEvolutionActive } from "@/lib/evolution"; +import { LiveStrip } from "@/components/ui/LiveStrip"; +import { StatusBadge } from "@/components/ui/StatusBadge"; + +/** 演化详情页头及 active run 操作。 */ +export function EvolutionRunHeader({ + run, + asOf, + isValidating, + isStaleFrame, + onAbort, +}: { + run: EvolutionRun; + asOf: string; + isValidating: boolean; + isStaleFrame: boolean; + onAbort: () => void; +}) { + const t = useTranslations("evolution.detail"); + const active = isEvolutionActive(run.status); + return ( +
+
+
+

{t("title")}

+ +
+

+ {run.run_id} · {run.seed_strategy_id} +

+
+
+ {active && ( + + )} + +
+
+ ); +} diff --git a/apps/dashboard/src/components/evolution/EvolutionRunTable.tsx b/apps/dashboard/src/components/evolution/EvolutionRunTable.tsx new file mode 100644 index 00000000..33df9274 --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionRunTable.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useLocale, useTranslations } from "next-intl"; + +import { Link } from "@/i18n/navigation"; +import type { EvolutionRunSummary } from "@/lib/types"; +import { cn } from "@/lib/cn"; +import { evolutionTone, isEvolutionActive } from "@/lib/evolution"; +import { fmtRelative } from "@/lib/format"; +import { Panel } from "@/components/ui/Panel"; +import { StatusBadge } from "@/components/ui/StatusBadge"; +import { Td, TableEmpty, TableHeadRow, Th } from "@/components/ui/Table"; + +type StatusFilter = "all" | "terminal" | "active"; +const FILTERS: StatusFilter[] = ["all", "terminal", "active"]; + +/** 演化运行筛选、表格和 keyset 加载入口。 */ +export function EvolutionRunTable({ + runs, + hasMore, + isLoadingMore, + onLoadMore, +}: { + runs: EvolutionRunSummary[]; + hasMore: boolean; + isLoadingMore: boolean; + onLoadMore: () => void; +}) { + const t = useTranslations("evolution"); + const locale = useLocale(); + const [filter, setFilter] = useState("all"); + const rows = useMemo( + () => filter === "all" ? runs : runs.filter((run) => filter === "active" ? isEvolutionActive(run.status) : !isEvolutionActive(run.status)), + [filter, runs], + ); + return ( + {FILTERS.map((value) => setFilter(value)} />)}}> + {rows.length === 0 ? {t("empty")} :
{rows.map((run) => )}
{t("col.run")}{t("col.status")}{t("col.seed")}{t("col.budget")}{t("col.candidates")}{t("col.rejected")}{t("col.cost")}{t("col.time")}
} + {hasMore &&
} +
+ ); +} + +function RunRow({ run, locale }: { run: EvolutionRunSummary; locale: string }) { + const ago = fmtRelative(run.finished_at ?? run.started_at ?? run.queued_at, Date.now(), locale); + return {run.run_id.slice(0, 8)}{run.seed_strategy_id}{run.budget}{run.succeeded}{run.rejected}${run.llm_cost_usd.toFixed(4)}{ago}; +} + +function FilterChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) { + return ; +} diff --git a/apps/dashboard/src/components/evolution/EvolutionStats.tsx b/apps/dashboard/src/components/evolution/EvolutionStats.tsx new file mode 100644 index 00000000..5081b4db --- /dev/null +++ b/apps/dashboard/src/components/evolution/EvolutionStats.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { STAT_TONE_CLASS } from "@/lib/evolution"; +import { cn } from "@/lib/cn"; + +/** 演化列表顶部聚合指标。 */ +export function EvolutionStats({ + total, + active, + cost, + rejected, +}: { + total: number; + active: number; + cost: number; + rejected: number; +}) { + const t = useTranslations("evolution"); + return ( +
+ + 0 ? "cyan" : "muted"} /> + + +
+ ); +} + +function Stat({ label, value, tone }: { label: string; value: string; tone: keyof typeof STAT_TONE_CLASS }) { + return
{label}
{value}
; +} diff --git a/apps/dashboard/src/lib/evolution-capability.ts b/apps/dashboard/src/lib/evolution-capability.ts new file mode 100644 index 00000000..7925997d --- /dev/null +++ b/apps/dashboard/src/lib/evolution-capability.ts @@ -0,0 +1,4 @@ +/** Evolver capability 可由部署显式关闭;本地默认启用。 */ +export function isEvolutionEnabled(): boolean { + return process.env.EVOLVER_ENABLED?.toLowerCase() !== "false"; +} diff --git a/apps/dashboard/src/lib/evolution-i18n.regression-1.test.ts b/apps/dashboard/src/lib/evolution-i18n.regression-1.test.ts new file mode 100644 index 00000000..48bd17b7 --- /dev/null +++ b/apps/dashboard/src/lib/evolution-i18n.regression-1.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +// Regression: ISSUE-001 — 演化页面缺少 chat.context.kind 文案 +// Found by /qa on 2026-08-14 +// Report: .gstack/qa-reports/qa-report-localhost-3001-2026-08-14.md +const EVOLUTION_KINDS = [ + "evolution_list", + "evolution_run_detail", + "evolution_candidate_detail", +] as const; + +function messages(locale: "zh" | "en") { + const path = resolve(process.cwd(), `messages/${locale}.json`); + return JSON.parse(readFileSync(path, "utf8")) as { + chat: { context: { kind: Record } }; + }; +} + +describe("evolution page-context messages", () => { + it.each(["zh", "en"] as const)("%s 覆盖全部演化页面类型", (locale) => { + const kinds = messages(locale).chat.context.kind; + for (const kind of EVOLUTION_KINDS) { + expect(kinds[kind]).toBeTruthy(); + } + }); +}); diff --git a/apps/dashboard/src/lib/evolution.test.ts b/apps/dashboard/src/lib/evolution.test.ts new file mode 100644 index 00000000..e7fa8f94 --- /dev/null +++ b/apps/dashboard/src/lib/evolution.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { + EVOLUTION_REFRESH_MS, + evolutionRefreshInterval, + evolutionTone, + isEvolutionActive, +} from "./evolution"; + +const ACTIVE = ["queued", "running", "cancelling"] as const; +const TERMINAL = ["completed", "failed", "aborted"] as const; + +describe("evolution status helpers", () => { + it("仅 active 状态继续轮询", () => { + for (const status of ACTIVE) { + expect(isEvolutionActive(status)).toBe(true); + expect(evolutionRefreshInterval(status)).toBe(EVOLUTION_REFRESH_MS); + } + for (const status of TERMINAL) { + expect(isEvolutionActive(status)).toBe(false); + expect(evolutionRefreshInterval(status)).toBe(0); + } + }); + + it("首帧未知时允许获取后续状态", () => { + expect(evolutionRefreshInterval(undefined)).toBe(EVOLUTION_REFRESH_MS); + }); + + it("状态颜色使用静态语义映射", () => { + expect(evolutionTone("completed")).toBe("bull"); + expect(evolutionTone("running")).toBe("cyan"); + expect(evolutionTone("queued")).toBe("gold"); + expect(evolutionTone("failed")).toBe("fox"); + expect(evolutionTone("aborted")).toBe("muted"); + }); +}); diff --git a/apps/dashboard/src/lib/evolution.ts b/apps/dashboard/src/lib/evolution.ts new file mode 100644 index 00000000..24316619 --- /dev/null +++ b/apps/dashboard/src/lib/evolution.ts @@ -0,0 +1,34 @@ +import type { EvolutionRunStatus } from "./types"; + +export const ACTIVE_EVOLUTION_STATUSES = new Set([ + "queued", + "running", + "cancelling", +]); + +export const EVOLUTION_REFRESH_MS = 4_000; + +export function evolutionRefreshInterval( + status: EvolutionRunStatus | undefined, +): number { + return status === undefined || isEvolutionActive(status) ? EVOLUTION_REFRESH_MS : 0; +} + +export function evolutionTone(status: EvolutionRunStatus) { + if (status === "completed") return "bull" as const; + if (status === "running") return "cyan" as const; + if (status === "queued" || status === "cancelling") return "gold" as const; + if (status === "failed") return "fox" as const; + return "muted" as const; +} + +export function isEvolutionActive(status: EvolutionRunStatus): boolean { + return ACTIVE_EVOLUTION_STATUSES.has(status); +} + +export const STAT_TONE_CLASS = { + bull: "text-bull", + cyan: "text-cyan", + fox: "text-fox-red", + muted: "text-fg-muted", +} as const; diff --git a/apps/dashboard/src/lib/evolver-backend.ts b/apps/dashboard/src/lib/evolver-backend.ts deleted file mode 100644 index 483b1ad6..00000000 --- a/apps/dashboard/src/lib/evolver-backend.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * evolver 服务后端 fetch 封装。 - * 由于 evolver 服务可能使用不同的 auth 方案(或当前无 auth), - * 这里用独立的后端 fetch,走 evolver 的 base URL。 - */ -import { BackendError, BACKENDS, getServiceToken } from "./backend"; - -interface FetchOptions { - query?: Record; - timeoutMs?: number; - auth?: boolean; - method?: "GET" | "POST"; - body?: unknown; -} - -export async function evolutionBackendFetch( - path: string, - opts: FetchOptions = {}, -): Promise { - const { query, timeoutMs = 10_000, auth = false, method = "GET", body } = opts; - const baseUrl = BACKENDS.evolver; - const url = new URL(path, baseUrl); - if (query) { - for (const [k, v] of Object.entries(query)) { - if (v !== undefined) url.searchParams.set(k, String(v)); - } - } - - const headers: Record = { Accept: "application/json" }; - if (auth) { - headers.Authorization = `Bearer ${await getServiceToken()}`; - } - if (body !== undefined) headers["Content-Type"] = "application/json"; - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - let res: Response; - try { - res = await fetch(url.toString(), { - method, - headers, - body: body !== undefined ? JSON.stringify(body) : undefined, - signal: controller.signal, - }); - } catch (err) { - throw new BackendError( - 0, - err instanceof Error ? err.message : `EVOLVER_UNREACHABLE`, - ); - } finally { - clearTimeout(timer); - } - - if (!res.ok) { - let detail: unknown; - try { - const body = await res.json(); - detail = body.detail ?? body; - } catch { - // ignore - } - throw new BackendError(res.status, `evolver ${res.status}`, detail); - } - - return (await res.json()) as T; -} \ No newline at end of file diff --git a/apps/dashboard/src/lib/page-context-shared.test.ts b/apps/dashboard/src/lib/page-context-shared.test.ts new file mode 100644 index 00000000..467ca68a --- /dev/null +++ b/apps/dashboard/src/lib/page-context-shared.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; + +import { + buildPageContextEnvelope, + parsePageContext, + stripPageContext, +} from "./page-context-shared"; + +const RUN_ID = "11111111-1111-4111-8111-111111111111"; +const CANDIDATE_ID = "22222222-2222-4222-8222-222222222222"; + +describe("evolution page context", () => { + it("解析列表、运行详情与候选详情", () => { + expect(parsePageContext("/evolution")).toEqual({ + kind: "evolution_list", + pathname: "/evolution", + }); + expect(parsePageContext(`/evolution/${RUN_ID}`)).toEqual({ + kind: "evolution_run_detail", + id: RUN_ID, + pathname: `/evolution/${RUN_ID}`, + }); + expect(parsePageContext(`/evolution/candidates/${CANDIDATE_ID}`)).toEqual({ + kind: "evolution_candidate_detail", + id: CANDIDATE_ID, + pathname: `/evolution/candidates/${CANDIDATE_ID}`, + }); + }); + + it("详情 envelope 使用演化专用 id 键", () => { + expect( + buildPageContextEnvelope({ + kind: "evolution_run_detail", + id: RUN_ID, + pathname: `/evolution/${RUN_ID}`, + }), + ).toContain(`evolution_run_id=${RUN_ID}`); + }); + + it("清除完整和被截断的上下文块", () => { + expect(stripPageContext("\npage=evolution_list\n\n\nhello")) + .toBe("hello"); + expect(stripPageContext("\npage=evolution_run_detail")).toBe(""); + }); +}); diff --git a/apps/dashboard/src/lib/page-context-shared.ts b/apps/dashboard/src/lib/page-context-shared.ts index f59ebcb5..0f2fa94e 100644 --- a/apps/dashboard/src/lib/page-context-shared.ts +++ b/apps/dashboard/src/lib/page-context-shared.ts @@ -16,6 +16,9 @@ export type PageKind = | "risk" | "activity" | "divination" + | "evolution_list" + | "evolution_run_detail" + | "evolution_candidate_detail" | "overview"; export interface PageContext { @@ -59,6 +62,17 @@ export function parsePageContext(pathname: string): PageContext { return { kind: "activity", pathname }; case "divination": return { kind: "divination", pathname }; + case "evolution": + if (second === "candidates" && segs[2] && UUID_RE.test(segs[2])) { + return { + kind: "evolution_candidate_detail", + id: segs[2], + pathname, + }; + } + return second && UUID_RE.test(second) + ? { kind: "evolution_run_detail", id: second, pathname } + : { kind: "evolution_list", pathname }; default: // 未知路由 → 当总览兜底,避免误带具体实体上下文。 return { kind: "overview", pathname }; @@ -74,6 +88,10 @@ export function buildPageContextEnvelope(ctx: PageContext): string { if (ctx.kind === "runner_detail" && ctx.id) lines.push(`run_id=${ctx.id}`); if (ctx.kind === "candidate_detail" && ctx.id) lines.push(`candidate_id=${ctx.id}`); + if (ctx.kind === "evolution_run_detail" && ctx.id) + lines.push(`evolution_run_id=${ctx.id}`); + if (ctx.kind === "evolution_candidate_detail" && ctx.id) + lines.push(`evolution_candidate_id=${ctx.id}`); lines.push(`path=${ctx.pathname}`); return `\n${lines.join("\n")}\n\n\n`; } diff --git a/apps/dashboard/src/lib/types.ts b/apps/dashboard/src/lib/types.ts index 522a5b59..a6d45dc6 100644 --- a/apps/dashboard/src/lib/types.ts +++ b/apps/dashboard/src/lib/types.ts @@ -467,7 +467,6 @@ export interface FactorsPayload { /** 统一活动流的事件类型(跨模块归一)。 */ export type ActivityKind = - | "scheduler" | "permission" | "decision" | "risk" @@ -528,53 +527,76 @@ export interface ActivityPayload { asOf: string; } -// ── E2 策略演化 ── +// ── E1 策略演化 ── + +export type EvolutionRunStatus = + | "queued" + | "running" + | "cancelling" + | "completed" + | "failed" + | "aborted"; -/** 演化运行中的候选摘要(对应 evolver CandidateResponse)。 */ export interface EvolutionCandidateSummary { candidate_id: string; run_id: string; + slot: number; generation: number; - parent_id: string | null; - source_hash: string; + stage: string; + outcome: string; + source_code: string | null; + source_hash: string | null; + unified_diff: string | null; mutation_hint: string | null; + llm_cost_usd: number | null; fitness: number | null; - report: Record | null; + evaluation_snapshot: Record | null; + audit_snapshot: Record | null; + contract_snapshot: Record | null; + error_code: string | null; + error_message: string | null; overfitting_risk: string; - status: string; created_at: string | null; + updated_at: string | null; } -/** 演化运行摘要(对应 evolver RunStatusResponse,不含候选列表)。 */ export interface EvolutionRunSummary { run_id: string; seed_strategy_id: string; budget: number; - config: Record | null; - status: string; + config: Record; + status: EvolutionRunStatus; + active_stage: string | null; llm_cost_usd: number; - candidates_count: number; - rejected_ast: number; - rejected_contract: number; - failed_eval: number; + queued_at: string; started_at: string | null; finished_at: string | null; + dataset_manifest: Record | null; + seed_report_snapshot: Record | null; + baseline_snapshot: Record | null; + failure_code: string | null; + failure_message: string | null; + attempted: number; + succeeded: number; + rejected: number; } -/** 演化运行详情=摘要 + 全部候选列表。 */ export interface EvolutionRun extends EvolutionRunSummary { candidates: EvolutionCandidateSummary[]; } -/** GET /api/evolution —— 演化运行列表负载。 */ export interface EvolutionPayload { runs: EvolutionRunSummary[]; - truncated: boolean; + nextCursor: string | null; asOf: string; } -/** GET /api/evolution/[runId] —— 单个演化运行详情负载。 */ export interface EvolutionRunDetailPayload { - run: EvolutionRun | null; + run: EvolutionRun; + asOf: string; +} + +export interface EvolutionCandidateDetailPayload { + candidate: EvolutionCandidateSummary; asOf: string; } diff --git a/apps/dashboard/src/lib/use-evolution-runs.ts b/apps/dashboard/src/lib/use-evolution-runs.ts new file mode 100644 index 00000000..c3336d41 --- /dev/null +++ b/apps/dashboard/src/lib/use-evolution-runs.ts @@ -0,0 +1,33 @@ +"use client"; + +import useSWRInfinite from "swr/infinite"; + +import type { EvolutionPayload } from "./types"; +import { jsonFetcher } from "./fetcher"; + +const PAGE_SIZE = 20; + +/** 分页读取演化运行,并保留 keyset cursor。 */ +export function useEvolutionRuns() { + const swr = useSWRInfinite( + (index, previous) => { + if (index > 0 && !previous?.nextCursor) return null; + const cursor = previous?.nextCursor; + const query = new URLSearchParams({ limit: String(PAGE_SIZE) }); + if (cursor) query.set("cursor", cursor); + return `/api/evolution?${query}`; + }, + jsonFetcher, + { refreshInterval: 30_000, keepPreviousData: true }, + ); + const pages = swr.data ?? []; + return { + ...swr, + runs: pages.flatMap((page) => page.runs), + asOf: pages[0]?.asOf, + hasMore: Boolean(pages.at(-1)?.nextCursor), + isLoadingMore: + swr.isValidating && pages.length > 0 && swr.size > pages.length, + loadMore: () => swr.setSize((size) => size + 1), + }; +} diff --git a/docs/04-current-state.md b/docs/04-current-state.md index 68ca1b8a..09184c70 100644 --- a/docs/04-current-state.md +++ b/docs/04-current-state.md @@ -3,7 +3,8 @@ > 状态:**D-12 因子库闭环完成(2026-06-11)**——因子血缘 + 衰减巡检 + monthly > 宏观 + 因子发现 L1,在 D-11(多市场模拟盘)/ D-10(web 搜索 + 财报基本面 + > 多市场数据)/ D-9(Plan/Exec 闭环 + LLM 自创策略 + 风控引擎)/ D-9.1a 基础上落地。 -> 下一里程碑:E2 多代演化(issue #7);research-hub(issue #6)已于 2026-06-12 收口(见下)。 +> 下一里程碑:E2 多代演化(issue #7);E1 单代生产闭环已落地(见下), +> research-hub(issue #6)已于 2026-06-12 收口。 > > 本文回答的问题:**clone 仓库后,"现在到底做到哪里、决策链路长什么样"。** > 详细架构与设计取舍见 [`docs/03-kernel-design.md`](./03-kernel-design.md); @@ -171,8 +172,10 @@ sequenceDiagram 港股 / 日英德 / 韩澳印 / 全球指数)生效——含节假日 / 午休 / 半日市 / DST,锁 粒度按交易所 code(issue #8 收口)。D-9 闭环完成。 -不在范围(下一阶段):MAP-Elites / Island Model / 多代演化 / unified-diff 变异(E2/E3); -`services/evolver/` 独立服务(等多代演化需求出现再拆)。 +不在范围(下一阶段):MAP-Elites / Island Model / 多代选择与 early stopping(E2/E3)。 +E1 已拆出 `services/evolver/` 独立服务,完成 unified-diff 单代变异、真实 frozen bars、 +seed / buy-and-hold / 全候选同数据哈希评估、owner 隔离、数据库幂等与异步 run/slot 状态机; +演化需用户显式授权,不会在候选采纳后自动产生额外 LLM 费用。 (注:promoted 候选的 live runner 原列在此处,已在 D-11 落地,见下方 D-11 小节。) --- @@ -486,7 +489,8 @@ spot 仍严格 long-only(裸空 / 超卖翻空被守门拒),做空 / 杠 > 重心:模拟盘(paper)先于实盘(live)。 -- **E2 多代演化**(issue #7 · ADR-0020):MAP-Elites / Island Model(E1 MVP 已上) +- **E2 多代演化**(issue #7):在已完成的 E1 单代生产闭环上增加 best-parent 多代选择与 + early stopping;MAP-Elites / Island Model 后置到真实运行数据证明需要时再做 - **delegation hop**(issue #5 · ADR-0012 补丁):sub-strategy 派生计划的转授权链 > 已收口:paper live runner(#1,D-11)、PnL 净口径 / 运行时长 TTL / build 退避(#45 / diff --git a/docs/designs/e1-production-closure.md b/docs/designs/e1-production-closure.md new file mode 100644 index 00000000..a100eab4 --- /dev/null +++ b/docs/designs/e1-production-closure.md @@ -0,0 +1,92 @@ +# Design: E1 生产闭环收口 + +Generated by /office-hours on 2026-08-18 +Branch: feat/e1-evolution-production +Repo: mirror29/inalpha +Status: APPROVED +Mode: Builder + +## Problem Statement + +E1 已验证真实异步状态机、失败路径、显式审批、幂等、owner 隔离和 Dashboard,但尚未在生产式环境产出一个成功候选。本地 Binance 上游不可达,当前分支还有 121 个文件差异,未经过完整代码审查、PR CI 和镜像实构建。此时进入 E2 会把基础链路问题和多代算法问题混在一起。 + +## What Makes This Cool + +用户通过自然语言明确授权一次演化,系统冻结同一份 fresh 已收盘行情,在同一 dataset hash 上评估 seed、市场基准和候选,并留下可复核的源码、diff、审计、指标与费用链路。成功不是“生成了代码”,而是候选能解释自己从哪里来、用什么数据得分、为何没有自动进入交易。 + +## Constraints + +- 不降低 freshness,不为本地网络写 Binance 特例。 +- 不自动 promote、start 或进入下单链路。 +- 新的计费 run 必须逐次获得明确授权;`budget=1` 表示 1 个候选,不是 1 美元。创建时冻结 provider、model、pricing basis/config digest,执行器必须使用该 snapshot,并在授权摘要与结果中展示。 +- E2 只做 N 代 best-parent + early stopping;MAP-Elites、Island Model 后置。 +- 先处理当前分支,不混入其它会话 WIP。 + +## Premises + +1. E1 完成定义必须包含至少一个生产式成功候选,而不只是正确失败。 +2. `api.binance.com` 本地不可达是环境阻断,不能靠 stale fallback 变绿。 +3. 代码审查、CI、镜像、部署和线上 smoke 是一个证据链,不能拆掉其中任一环节后宣布完成。 +4. 在 E1 成功率、拒绝分布和费用没有真实样本前,不应设计复杂 E2 搜索算法。 + +## Approaches Considered + +- **审查后线上冒烟(选择)**:先 review 和修复,再 PR/CI/镜像,部署新加坡后跑 BTC/USDT 成功闭环。覆盖真实用户路径且不污染 connector。 +- **先修本地上游(拒绝)**:更早拿到成功结果,但容易把地域网络问题误做成产品代码。 +- **换市场双重验证(保留为补充)**:能证明全球市场通用性,但不能代替主要 BTC 路径。 + +## Recommended Approach + +### 1. 审查与合并门 + +- 对 `main...HEAD` 做高强度代码审查,重点看金融时间、状态竞态、owner SQL、审批边界、迁移和部署接线。 +- 修复所有 confirmed must-fix;把 Paper/Evolver pytest 加入 required CI,复跑全套 TS/Python 测试、Ruff、consistency、Compose;migration downgrade/upgrade 只在 disposable `_test` DB 执行。 +- 本地实构建 Evolver 镜像,验证容器能启动并 spawn 回测子进程。 +- 提 PR,等待 CI 和 auto-review;合并前读取评论正文,不只看绿色 check。 +- 合并后等待镜像工作流完成,以 merge SHA 固定 `IMAGE_TAG`,记录 digest,禁止本次使用 `latest`。 + +### 2. 部署与成功冒烟 + +- 部署前取 ≤24h 的 pg_dump,校验本地与 R2 对象,并在 disposable DB 做恢复验证;记录旧镜像 SHA。migration 失败立即停止。应用可回退旧 SHA,schema 保持前向兼容,不自动 destructive downgrade。 +- 以 merge SHA 镜像部署腾讯云新加坡,并记录容器 digest、migration revision 和 health。 +- 使用没有正式 candidate、runner、订单的专用 smoke owner。用户重新授权固定业务请求和冻结的 LLM snapshot:`budget=1 candidate`、预计费用、`sma_cross_v1 / binance / BTC/USDT / 1h / 30d` 与 idempotency key。 +- 提交时 `now-requested_as_of<=5m`;`queued->started<=5m`;单回测沿用配置的 300s timeout,整个 smoke 20m 未终态即判失败。超限不改写 as_of、不自动新建 run,是否 abort 另行确认。 +- 验证状态链。成功必须同时满足 `run.status=completed`、`succeeded>=1`、至少一个 slot `outcome=succeeded`,且源码、diff、audit、fitness、evaluation 均可读;failed、aborted、超时或 completed+0 均失败,不自动重跑。 +- 行情 freshness:latest bar 必须等于市场日历推导的最近已收盘 bar。保存 as_of、cutoff、latest ts、lag、manifest 和 dataset hash,不用 bar 数量判新鲜。 +- 保存 smoke 时间窗内 Mastra Docker JSON logs、JWT sub→smoke owner 映射;按 session、run/candidate ID 过滤 `paper.*`/`live.*`,并比较该 owner 部署前后的正式 candidate、runner、order DB 快照。两类证据均为零增量才证明没有 promote/start/order。 + +### 3. E1 关闭门与 E2 观察门 + +- **E1 关闭门**:一个逐次授权的成功候选和完整证据包即可关闭本里程碑。 +- **E2 观察门**:之后每个新 run 仍单独确认参数、候选预算和费用;累计 3–5 个不同市场/周期样本,记录成功率、阶段拒绝分布、耗时和费用,再单独设计 E2。 + +## Success Criteria + +- required CI 含 Paper/Evolver pytest;本地镜像、Compose、migration 往返全通过。 +- 新加坡部署使用 merge SHA 镜像,备份、digest、revision 和回滚点已留证。 +- 新加坡至少一个 run 满足 `completed + succeeded>=1 + slot succeeded`,候选可完整追溯。 +- seed、baseline、候选共享 dataset hash 与年化口径;as_of/queue 阈值与 latest closed bar freshness 均通过;执行模型等于创建时冻结的 LLM snapshot。 +- stale/上游失败仍 fail closed;失败或超时不自动创建新计费 run。 +- 跨 owner 404;同 key 同 body 不重复计费。 +- 专用 smoke owner 的关联审计证明 promote/start/order 调用数为 0。 + +## Distribution Plan + +GitHub Actions 在 main 合并后构建 Evolver 镜像;等待 merge SHA 构建成功,再钉该 SHA 经现有腾讯云 Docker Compose 链路发布。Dashboard 与 Mastra 通过容器内 `EVOLVER_SERVICE_URL` 使用服务,不新增分发渠道。 + +## Next Steps + +1. 立即对当前分支运行 `/review`,不是继续写 E2。 +2. 按 finding 拆分修复提交并复跑红线。 +3. 使用 `/ship` 创建 PR,检查 CI 与 auto-review。 +4. 合并部署后申请一次新的演化授权,完成线上成功冒烟。 +5. 记录 3–5 个真实 run 的成功率、拒绝分布、耗时和费用,再写 E2 计划。 + +## The Assignment + +下一步只做一件事:对 `main...feat/e1-evolution-production` 运行高强度 `/review`,把 review 结论作为是否进入 PR 的门。 + +## What I noticed about how you think + +- 你选了“先收口 E1”,没有把“失败路径正确”偷换成“闭环完成”。 +- 你选了“审查后线上冒烟”,意味着测试目标是用户真实路径,不是为本地网络把结果做绿。 diff --git a/infra/.env.prod.example b/infra/.env.prod.example index cee7d7f0..c508229a 100644 --- a/infra/.env.prod.example +++ b/infra/.env.prod.example @@ -32,6 +32,8 @@ DATA_SERVICE_URL=http://data:8001 PAPER_SERVICE_URL=http://paper:8002 RESEARCH_SERVICE_URL=http://research:8003 FACTOR_SERVICE_URL=http://factor:8004 +EVOLVER_SERVICE_URL=http://evolver:8005 +EVOLVER_ENABLED=true MASTRA_URL=http://mastra:4111 # ---- 鉴权(dashboard BFF ↔ 后端)---- @@ -49,6 +51,12 @@ ANTHROPIC_API_KEY= OPENAI_API_KEY= GEMINI_API_KEY= +# ---- E1 演化运行时 ---- +EVOLVER_MAX_RUNNING_RUNS=1 +EVOLVER_ACCOUNT_ACTIVE_LIMIT=2 +EVOLVER_JOB_TIMEOUT_S=300 +EVOLVER_JOB_MEM_GB=2 + # ---- 行情数据源 ---- BINANCE_API_KEY= BINANCE_API_SECRET= diff --git a/infra/.env.selfhost.example b/infra/.env.selfhost.example index 3cd91265..0a16b8b2 100644 --- a/infra/.env.selfhost.example +++ b/infra/.env.selfhost.example @@ -27,6 +27,14 @@ DATA_SERVICE_URL=http://data:8001 PAPER_SERVICE_URL=http://paper:8002 RESEARCH_SERVICE_URL=http://research:8003 FACTOR_SERVICE_URL=http://factor:8004 +EVOLVER_SERVICE_URL=http://evolver:8005 +EVOLVER_ENABLED=true + +# E1 evolution runtime (single worker / replica) +EVOLVER_MAX_RUNNING_RUNS=1 +EVOLVER_ACCOUNT_ACTIVE_LIMIT=2 +EVOLVER_JOB_TIMEOUT_S=300 +EVOLVER_JOB_MEM_GB=2 # Each authenticated user configures their own LLM API key in the dashboard. # Do not add provider API keys here as a shared fallback. diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml index 03b1e7fb..6f75388d 100644 --- a/infra/docker-compose.prod.yml +++ b/infra/docker-compose.prod.yml @@ -6,7 +6,7 @@ name: inalpha-prod # 迁移:migrate 服务一次性跑 alembic upgrade head(随 up 自动先跑) # 停: docker compose -f infra/docker-compose.prod.yml down # -# 拓扑:postgres + redis + migrate(一次性) + data/paper/research/factor + mastra + dashboard; +# 拓扑:postgres + redis + migrate(一次性) + data/paper/research/factor/evolver + mastra + dashboard; # --profile tunnel 时另启 cloudflared。 # 仅 cloudflared 对公网;其余服务走容器内网,按服务名互访。 # @@ -147,6 +147,30 @@ services: retries: 5 start_period: 40s + evolver: + <<: *svc-common + image: ${IMAGE_PREFIX:-ghcr.io/__CHANGE_ME__}/inalpha-evolver:${IMAGE_TAG:-latest} + build: + context: ../services + dockerfile: ../infra/docker/Dockerfile.evolver + container_name: inalpha-evolver + environment: + PORT: 8005 + WORKERS: 1 + DATA_SERVICE_URL: http://data:8001 + deploy: + replicas: 1 + depends_on: + postgres: { condition: service_healthy } + migrate: { condition: service_completed_successfully } + data: { condition: service_healthy } + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8005/health', timeout=2)"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 30s + mastra: image: ${IMAGE_PREFIX:-ghcr.io/__CHANGE_ME__}/inalpha-mastra:${IMAGE_TAG:-latest} build: @@ -162,7 +186,8 @@ services: PAPER_SERVICE_URL: http://paper:8002 RESEARCH_SERVICE_URL: http://research:8003 FACTOR_SERVICE_URL: http://factor:8004 - depends_on: [data, paper, research, factor] + EVOLVER_SERVICE_URL: http://evolver:8005 + depends_on: [data, paper, research, factor, evolver] volumes: - mastra_data:/app/.data # @mastra/libsql 本地存储持久化(paths.ts resolveMastraDbDir() 返回 .data/ 而非 .mastra/) - mastra_backups:/app/.data-backups # 每日自动备份轮转持久化 @@ -184,7 +209,7 @@ services: container_name: inalpha-dashboard restart: unless-stopped # 凭据隔离(最小权限):dashboard 是公网前端,**不挂 env_file** —— 那会把 DB/Redis/LLM/ - # Binance/tunnel 全部凭据注入前端容器(RCE 即全泄)。BFF 实际只需 JWT_SECRET + 5 个内网 URL; + # Binance/tunnel 全部凭据注入前端容器(RCE 即全泄)。BFF 实际只需 JWT_SECRET + 6 个内网 URL; # JWT_SECRET 经 compose 从 .env.prod 插值,只此一个 secret 进容器。 environment: NODE_ENV: production @@ -206,6 +231,8 @@ services: DATA_SERVICE_URL: http://data:8001 RESEARCH_SERVICE_URL: http://research:8003 FACTOR_SERVICE_URL: http://factor:8004 + EVOLVER_SERVICE_URL: http://evolver:8005 + EVOLVER_ENABLED: ${EVOLVER_ENABLED:-true} # 等后端 healthy 再起,避免冷启动期 BFF 路由全 502 depends_on: mastra: { condition: service_healthy } @@ -213,6 +240,7 @@ services: data: { condition: service_healthy } research: { condition: service_healthy } factor: { condition: service_healthy } + evolver: { condition: service_healthy } healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:3001/en').then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))"] interval: 10s diff --git a/infra/docker-compose.selfhost.yml b/infra/docker-compose.selfhost.yml index 5d30f059..bad8cd36 100644 --- a/infra/docker-compose.selfhost.yml +++ b/infra/docker-compose.selfhost.yml @@ -20,6 +20,9 @@ services: factor: container_name: inalpha-selfhost-factor build: {context: ../services, dockerfile: ../infra/docker/Dockerfile.python, args: {SERVICE: factor}} + evolver: + container_name: inalpha-selfhost-evolver + build: {context: ../services, dockerfile: ../infra/docker/Dockerfile.evolver} mastra: container_name: inalpha-selfhost-mastra build: {context: ../packages/orchestration, dockerfile: ../../infra/docker/Dockerfile.mastra} diff --git a/infra/docker/Dockerfile.evolver b/infra/docker/Dockerfile.evolver new file mode 100644 index 00000000..db861671 --- /dev/null +++ b/infra/docker/Dockerfile.evolver @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 +# Evolver 依赖 paper 评估 seam 与两层 shared 包,需保留源码相对路径。 +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +WORKDIR /app +RUN useradd --create-home --uid 10001 app && chown app:app /app +USER app + +COPY --chown=app:app _shared/pyproject.toml _shared/uv.lock ./_shared/ +COPY --chown=app:app _shared/llm/pyproject.toml ./_shared/llm/ +COPY --chown=app:app paper/pyproject.toml paper/uv.lock ./paper/ +COPY --chown=app:app evolver/pyproject.toml evolver/uv.lock ./evolver/ +COPY --chown=app:app _shared ./_shared +COPY --chown=app:app paper ./paper +COPY --chown=app:app evolver ./evolver + +WORKDIR /app/evolver +RUN uv sync --frozen --no-dev + +ENV PYTHONUNBUFFERED=1 +CMD ["sh", "-c", "uv run uvicorn inalpha_evolver.main:app --host 0.0.0.0 --port ${PORT:-8005} --workers 1"] diff --git a/infra/migrations/evolution_e1_candidate_sql.py b/infra/migrations/evolution_e1_candidate_sql.py new file mode 100644 index 00000000..99d46637 --- /dev/null +++ b/infra/migrations/evolution_e1_candidate_sql.py @@ -0,0 +1,68 @@ +CANDIDATE_UP = ( + """ALTER TABLE strategy_evo_candidates + ALTER COLUMN source_code DROP NOT NULL, + ALTER COLUMN source_hash DROP NOT NULL, + ALTER COLUMN report DROP NOT NULL, + ALTER COLUMN data_epoch DROP NOT NULL, + ADD COLUMN slot INT, ADD COLUMN stage TEXT, ADD COLUMN outcome TEXT, + ADD COLUMN audit_snapshot JSONB, ADD COLUMN contract_snapshot JSONB, + ADD COLUMN evaluation_snapshot JSONB, ADD COLUMN error_code TEXT, + ADD COLUMN error_message TEXT, ADD COLUMN input_tokens INT, + ADD COLUMN output_tokens INT, ADD COLUMN updated_at TIMESTAMPTZ;""", + """WITH ranked AS ( + SELECT candidate_id, + row_number() OVER (PARTITION BY run_id ORDER BY created_at, candidate_id) - 1 AS n + FROM strategy_evo_candidates + ) + UPDATE strategy_evo_candidates AS candidate SET slot = ranked.n, + stage = 'legacy', outcome = 'legacy_unknown', + evaluation_snapshot = candidate.report, updated_at = candidate.created_at + FROM ranked WHERE candidate.candidate_id = ranked.candidate_id;""", + """ALTER TABLE strategy_evo_candidates + ADD CONSTRAINT evo_candidate_required_fields_check + CHECK (slot IS NOT NULL AND stage IS NOT NULL AND outcome IS NOT NULL + AND updated_at IS NOT NULL) NOT VALID; + ALTER TABLE strategy_evo_candidates + VALIDATE CONSTRAINT evo_candidate_required_fields_check; + ALTER TABLE strategy_evo_candidates + ALTER COLUMN slot SET NOT NULL, + ALTER COLUMN stage SET DEFAULT 'queued', ALTER COLUMN stage SET NOT NULL, + ALTER COLUMN outcome SET DEFAULT 'pending', ALTER COLUMN outcome SET NOT NULL, + ALTER COLUMN updated_at SET DEFAULT NOW(), ALTER COLUMN updated_at SET NOT NULL; + ALTER TABLE strategy_evo_candidates + DROP CONSTRAINT evo_candidate_required_fields_check; + ALTER TABLE strategy_evo_candidates ADD CONSTRAINT evo_candidate_run_slot_key + UNIQUE (run_id, slot);""", + """ALTER TABLE strategy_evo_candidates ADD CONSTRAINT evo_candidate_stage_check + CHECK (stage IN ('legacy','queued','mutation','evaluation','completed')) NOT VALID; + ALTER TABLE strategy_evo_candidates ADD CONSTRAINT evo_candidate_outcome_check + CHECK (outcome IN ('legacy_unknown','pending','mutation_failed','no_change', + 'diff_failed','ast_rejected','contract_rejected','duplicate', + 'evaluation_failed','succeeded','cancelled')) NOT VALID; + ALTER TABLE strategy_evo_candidates ADD CONSTRAINT evo_candidate_success_check + CHECK (outcome <> 'succeeded' OR (stage = 'completed' + AND source_code IS NOT NULL AND source_hash IS NOT NULL + AND evaluation_snapshot IS NOT NULL AND fitness IS NOT NULL + AND data_epoch IS NOT NULL AND report IS NOT NULL)) NOT VALID; + ALTER TABLE strategy_evo_candidates VALIDATE CONSTRAINT evo_candidate_stage_check; + ALTER TABLE strategy_evo_candidates VALIDATE CONSTRAINT evo_candidate_outcome_check; + ALTER TABLE strategy_evo_candidates VALIDATE CONSTRAINT evo_candidate_success_check;""", +) + +CANDIDATE_DOWN = ( + """ALTER TABLE strategy_evo_candidates + DROP CONSTRAINT evo_candidate_success_check, + DROP CONSTRAINT evo_candidate_outcome_check, + DROP CONSTRAINT evo_candidate_stage_check, + DROP CONSTRAINT evo_candidate_run_slot_key;""", + """ALTER TABLE strategy_evo_candidates + DROP COLUMN slot, DROP COLUMN stage, DROP COLUMN outcome, + DROP COLUMN audit_snapshot, DROP COLUMN contract_snapshot, + DROP COLUMN evaluation_snapshot, DROP COLUMN error_code, + DROP COLUMN error_message, DROP COLUMN input_tokens, + DROP COLUMN output_tokens, DROP COLUMN updated_at, + ALTER COLUMN source_code SET NOT NULL, + ALTER COLUMN source_hash SET NOT NULL, + ALTER COLUMN report SET NOT NULL, + ALTER COLUMN data_epoch SET NOT NULL;""", +) diff --git a/infra/migrations/evolution_e1_run_down_sql.py b/infra/migrations/evolution_e1_run_down_sql.py new file mode 100644 index 00000000..9c4a7922 --- /dev/null +++ b/infra/migrations/evolution_e1_run_down_sql.py @@ -0,0 +1,18 @@ +RUN_DOWN = ( + "DROP INDEX IF EXISTS idx_er_active; DROP INDEX IF EXISTS idx_er_owner_cursor;", + """ALTER TABLE strategy_evo_runs + DROP CONSTRAINT evo_run_owner_idempotency_key, + DROP CONSTRAINT strategy_evo_runs_status_check, + DROP COLUMN owner_account_id, DROP COLUMN requested_by_sub, + DROP COLUMN idempotency_key, DROP COLUMN request_hash, DROP COLUMN queued_at, + DROP COLUMN queued_at_is_estimate, DROP COLUMN updated_at, DROP COLUMN venue, + DROP COLUMN symbol, DROP COLUMN request_timeframe, DROP COLUMN data_timeframe, + DROP COLUMN engine_timeframe, DROP COLUMN requested_as_of, + DROP COLUMN seed_source_snapshot, DROP COLUMN seed_source_hash, + DROP COLUMN seed_report_snapshot, DROP COLUMN baseline_snapshot, + DROP COLUMN dataset_manifest, DROP COLUMN active_stage, DROP COLUMN failure_code, + DROP COLUMN failure_message, ALTER COLUMN started_at SET NOT NULL, + ALTER COLUMN status SET DEFAULT 'running'; + ALTER TABLE strategy_evo_runs ADD CONSTRAINT strategy_evo_runs_status_check + CHECK (status IN ('running','completed','failed','aborted'));""", +) diff --git a/infra/migrations/evolution_e1_run_sql.py b/infra/migrations/evolution_e1_run_sql.py new file mode 100644 index 00000000..fdb33a9b --- /dev/null +++ b/infra/migrations/evolution_e1_run_sql.py @@ -0,0 +1,51 @@ +RUN_UP = ( + """ALTER TABLE strategy_evo_runs + DROP CONSTRAINT IF EXISTS strategy_evo_runs_status_check; + ALTER TABLE strategy_evo_runs + ADD COLUMN owner_account_id UUID, ADD COLUMN requested_by_sub TEXT, + ADD COLUMN idempotency_key TEXT, ADD COLUMN request_hash TEXT, + ADD COLUMN queued_at TIMESTAMPTZ, ADD COLUMN queued_at_is_estimate BOOLEAN, + ADD COLUMN updated_at TIMESTAMPTZ, ADD COLUMN venue TEXT, ADD COLUMN symbol TEXT, + ADD COLUMN request_timeframe TEXT, ADD COLUMN data_timeframe TEXT, + ADD COLUMN engine_timeframe TEXT, ADD COLUMN requested_as_of TIMESTAMPTZ, + ADD COLUMN seed_source_snapshot TEXT, ADD COLUMN seed_source_hash TEXT, + ADD COLUMN seed_report_snapshot JSONB, ADD COLUMN baseline_snapshot JSONB, + ADD COLUMN dataset_manifest JSONB, ADD COLUMN active_stage TEXT, + ADD COLUMN failure_code TEXT, ADD COLUMN failure_message TEXT; + ALTER TABLE strategy_evo_runs ALTER COLUMN started_at DROP NOT NULL;""", + """UPDATE strategy_evo_runs SET + owner_account_id = '00000000-0000-0000-0000-000000000001', + requested_by_sub = 'legacy', idempotency_key = 'legacy:' || run_id, + request_hash = md5(config::text), queued_at = started_at, + queued_at_is_estimate = TRUE, updated_at = COALESCE(finished_at, started_at), + venue = NULLIF(config->>'venue', ''), + symbol = COALESCE(NULLIF(config->>'symbol', ''), NULLIF(config->'universe'->>0, '')), + request_timeframe = NULLIF(config->>'timeframe', ''), + data_timeframe = NULLIF(config->>'timeframe', ''), + engine_timeframe = NULLIF(config->>'timeframe', ''), active_stage = 'legacy';""", + """ALTER TABLE strategy_evo_runs ADD CONSTRAINT evo_run_required_fields_check + CHECK (owner_account_id IS NOT NULL AND requested_by_sub IS NOT NULL + AND idempotency_key IS NOT NULL AND request_hash IS NOT NULL + AND queued_at IS NOT NULL AND queued_at_is_estimate IS NOT NULL + AND updated_at IS NOT NULL) NOT VALID; + ALTER TABLE strategy_evo_runs VALIDATE CONSTRAINT evo_run_required_fields_check; + ALTER TABLE strategy_evo_runs + ALTER COLUMN owner_account_id SET NOT NULL, + ALTER COLUMN requested_by_sub SET NOT NULL, + ALTER COLUMN idempotency_key SET NOT NULL, + ALTER COLUMN request_hash SET NOT NULL, + ALTER COLUMN queued_at SET NOT NULL, + ALTER COLUMN queued_at_is_estimate SET DEFAULT FALSE, + ALTER COLUMN queued_at_is_estimate SET NOT NULL, + ALTER COLUMN updated_at SET DEFAULT NOW(), + ALTER COLUMN updated_at SET NOT NULL, + ALTER COLUMN status SET DEFAULT 'queued'; + ALTER TABLE strategy_evo_runs DROP CONSTRAINT evo_run_required_fields_check; + ALTER TABLE strategy_evo_runs ADD CONSTRAINT strategy_evo_runs_status_check + CHECK (status IN ('queued','running','cancelling','completed','failed','aborted')); + ALTER TABLE strategy_evo_runs ADD CONSTRAINT evo_run_owner_idempotency_key + UNIQUE (owner_account_id, idempotency_key);""", + """CREATE INDEX idx_er_owner_cursor + ON strategy_evo_runs (owner_account_id, queued_at DESC, run_id DESC); + CREATE INDEX idx_er_active ON strategy_evo_runs (status, queued_at);""", +) diff --git a/infra/migrations/pyproject.toml b/infra/migrations/pyproject.toml index 2542f3d8..d2b49f54 100644 --- a/infra/migrations/pyproject.toml +++ b/infra/migrations/pyproject.toml @@ -9,3 +9,17 @@ dependencies = [ "psycopg[binary]>=3.3.0", "python-dotenv>=1.2.0", ] + +[dependency-groups] +dev = [ + "pytest>=9.0.0", + "ruff>=0.15.0", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --strict-markers" +markers = ["integration: requires a dedicated PostgreSQL *_test database"] + +[tool.ruff] +line-length = 88 diff --git a/infra/migrations/tests/conftest.py b/infra/migrations/tests/conftest.py new file mode 100644 index 00000000..6597698e --- /dev/null +++ b/infra/migrations/tests/conftest.py @@ -0,0 +1,72 @@ +"""Dedicated PostgreSQL fixtures for real Alembic migration tests.""" + +from __future__ import annotations + +import os +from collections.abc import Iterator +from urllib.parse import urlsplit, urlunsplit + +import psycopg +import pytest +from psycopg import sql + +_DEFAULT_URL = ( + "postgresql+psycopg://quant:devpass@localhost:5433/" + "inalpha_migration_test" +) + + +def _psycopg_url(url: str) -> str: + """Convert SQLAlchemy's explicit psycopg URL into a libpq URL.""" + return url.replace("postgresql+psycopg://", "postgresql://", 1) + + +def _database_name(url: str) -> str: + return urlsplit(url).path.removeprefix("/") + + +def _assert_dedicated_test_database(url: str) -> None: + """Reject every database except the migration suite's dedicated *_test DB.""" + database = _database_name(url) + if not database.startswith("inalpha_migration_") or not database.endswith("_test"): + raise pytest.UsageError( + "INALPHA_MIGRATION_TEST_DATABASE_URL must name " + "inalpha_migration_*_test; development/production databases are forbidden" + ) + + +def _provision_test_database(url: str) -> None: + """Create only the guarded test database through PostgreSQL's maintenance DB.""" + parsed = urlsplit(_psycopg_url(url)) + if parsed.hostname not in {"localhost", "127.0.0.1", "::1"}: + raise pytest.UsageError("automatic test database creation is localhost-only") + admin_url = urlunsplit(parsed._replace(path="/postgres")) + database = _database_name(url) + with psycopg.connect(admin_url, autocommit=True) as admin: + exists = admin.execute( + "SELECT 1 FROM pg_database WHERE datname=%s", (database,) + ).fetchone() + if exists is None: + admin.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(database))) + + +@pytest.fixture(scope="session") +def migration_db_url() -> Iterator[str]: + """Reset only an explicitly dedicated migration test database.""" + configured_url = os.environ.get("INALPHA_MIGRATION_TEST_DATABASE_URL") + url = configured_url or _DEFAULT_URL + _assert_dedicated_test_database(url) + try: + if configured_url is None: + _provision_test_database(url) + connection = psycopg.connect(_psycopg_url(url), autocommit=True) + except psycopg.OperationalError as exc: + pytest.skip(f"dedicated migration test database is unavailable: {exc}") + + with connection: + connection.execute("DROP SCHEMA public CASCADE") + connection.execute("CREATE SCHEMA public") + yield url + with psycopg.connect(_psycopg_url(url), autocommit=True) as cleanup: + cleanup.execute("DROP SCHEMA public CASCADE") + cleanup.execute("CREATE SCHEMA public") diff --git a/infra/migrations/tests/migration_0038_support.py b/infra/migrations/tests/migration_0038_support.py new file mode 100644 index 00000000..bf99402b --- /dev/null +++ b/infra/migrations/tests/migration_0038_support.py @@ -0,0 +1,81 @@ +"""Shared operations for the 0038 PostgreSQL migration test.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +import psycopg + +ROOT = Path(__file__).resolve().parents[1] +RUN_LEGACY = "10000000-0000-0000-0000-000000000001" +RUN_QUEUED = "10000000-0000-0000-0000-000000000002" +CANDIDATE_LEGACY = "20000000-0000-0000-0000-000000000001" +CANDIDATE_FAILED = "20000000-0000-0000-0000-000000000002" + + +def db_url(url: str) -> str: + """Convert an SQLAlchemy psycopg URL to a libpq URL.""" + return url.replace("postgresql+psycopg://", "postgresql://", 1) + + +def alembic( + url: str, *args: str, check: bool = True +) -> subprocess.CompletedProcess[str]: + """Run the real Alembic CLI against only the supplied test database.""" + return subprocess.run( + [sys.executable, "-m", "alembic", "-c", "alembic.ini", *args], + cwd=ROOT, + env={**os.environ, "DATABASE_URL": url}, + check=check, + capture_output=True, + text=True, + ) + + +def seed_legacy_rows(conn: psycopg.Connection[tuple[object, ...]]) -> None: + """Insert rows valid at 0037, including a candidate with NULL fitness.""" + conn.execute( + """INSERT INTO strategy_evo_runs + (run_id,seed_strategy_id,budget,config,status,started_at,finished_at) + VALUES (%s,'legacy-seed',1,'{"venue":"legacy-market","timeframe":"1d"}', + 'completed','2025-01-02T03:04:05Z','2025-01-03T04:05:06Z')""", + (RUN_LEGACY,), + ) + conn.execute( + """INSERT INTO strategy_evo_candidates + (candidate_id,run_id,generation,source_code,source_hash,fitness,report, + data_epoch,status,created_at) VALUES + (%s,%s,1,'legacy source','legacy-hash',NULL,'{"legacy":true}', + 1735776000000,'rejected','2025-01-02T05:00:00Z')""", + (CANDIDATE_LEGACY, RUN_LEGACY), + ) + + +def seed_incompatible_rows(conn: psycopg.Connection[tuple[object, ...]]) -> None: + """Insert a queued run and failed candidate that 0037 cannot represent.""" + conn.execute( + """INSERT INTO strategy_evo_runs + (run_id,owner_account_id,requested_by_sub,seed_strategy_id,budget,config, + status,idempotency_key,request_hash,queued_at,started_at) + VALUES (%s,'00000000-0000-0000-0000-000000000002','test','seed',1, + '{}','queued','queued-test','hash','2026-01-01T00:00:00Z',NULL)""", + (RUN_QUEUED,), + ) + conn.execute( + """INSERT INTO strategy_evo_candidates + (candidate_id,run_id,slot,generation,stage,outcome,status) + VALUES (%s,%s,0,1,'completed','mutation_failed','evaluated')""", + (CANDIDATE_FAILED, RUN_QUEUED), + ) + + +def remove_incompatible_rows(conn: psycopg.Connection[tuple[object, ...]]) -> None: + """Remove only test fixtures so the representable downgrade path can run.""" + conn.execute( + "DELETE FROM strategy_evo_candidates WHERE candidate_id=%s", + (CANDIDATE_FAILED,), + ) + conn.execute("DELETE FROM strategy_evo_runs WHERE run_id=%s", (RUN_QUEUED,)) diff --git a/infra/migrations/tests/test_migration_0038.py b/infra/migrations/tests/test_migration_0038.py new file mode 100644 index 00000000..d3bdb19e --- /dev/null +++ b/infra/migrations/tests/test_migration_0038.py @@ -0,0 +1,90 @@ +"""0038 E1 schema migration tests against real PostgreSQL.""" + +from __future__ import annotations + +import psycopg +import pytest +from migration_0038_support import ( + CANDIDATE_LEGACY, + RUN_LEGACY, + alembic, + db_url, + remove_incompatible_rows, + seed_incompatible_rows, + seed_legacy_rows, +) + +pytestmark = pytest.mark.integration + + +def test_0038_upgrade_downgrade_upgrade_preserves_truth( + migration_db_url: str, +) -> None: + """Legacy NULL fitness upgrades; unsafe downgrade refuses without deletion.""" + alembic(migration_db_url, "upgrade", "0037") + with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn: + seed_legacy_rows(conn) + + alembic(migration_db_url, "upgrade", "0038") + with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn: + legacy_run = conn.execute( + """SELECT status,active_stage,queued_at,started_at,finished_at, + requested_as_of,queued_at_is_estimate FROM strategy_evo_runs + WHERE run_id=%s""", + (RUN_LEGACY,), + ).fetchone() + assert legacy_run is not None + assert legacy_run[0:2] == ("completed", "legacy") + assert legacy_run[2] == legacy_run[3] + assert legacy_run[4] is not None and legacy_run[5] is None + assert legacy_run[6] is True + assert conn.execute( + """SELECT stage,outcome,fitness,evaluation_snapshot + FROM strategy_evo_candidates WHERE candidate_id=%s""", + (CANDIDATE_LEGACY,), + ).fetchone() == ("legacy", "legacy_unknown", None, {"legacy": True}) + definition = conn.execute( + """SELECT pg_get_constraintdef(oid) FROM pg_constraint + WHERE conname='evo_candidate_success_check'""" + ).fetchone()[0] + required = ( + "source_code", "source_hash", "evaluation_snapshot", + "fitness", "data_epoch", "report", + ) + assert all(field in definition for field in required) + with pytest.raises(psycopg.errors.CheckViolation): + conn.execute( + "UPDATE strategy_evo_candidates SET outcome='succeeded' WHERE candidate_id=%s", + (CANDIDATE_LEGACY,), + ) + seed_incompatible_rows(conn) + + failed = alembic(migration_db_url, "downgrade", "0037", check=False) + assert failed.returncode != 0 + assert "0038 downgrade refused" in failed.stderr + with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn: + assert conn.execute("SELECT version_num FROM alembic_version").fetchone() == ( + "0038", + ) + assert conn.execute( + "SELECT count(*) FROM strategy_evo_candidates" + ).fetchone() == (2,) + remove_incompatible_rows(conn) + + alembic(migration_db_url, "downgrade", "0037") + with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn: + row = conn.execute( + """SELECT fitness,r.started_at,r.finished_at + FROM strategy_evo_candidates c JOIN strategy_evo_runs r USING(run_id) + WHERE candidate_id=%s""", + (CANDIDATE_LEGACY,), + ).fetchone() + assert row is not None and row[0] is None + assert row[1:] == legacy_run[3:5] + + alembic(migration_db_url, "upgrade", "0038") + with psycopg.connect(db_url(migration_db_url)) as conn: + assert conn.execute( + "SELECT stage,outcome FROM strategy_evo_candidates WHERE candidate_id=%s", + (CANDIDATE_LEGACY,), + ).fetchone() == ("legacy", "legacy_unknown") diff --git a/infra/migrations/uv.lock b/infra/migrations/uv.lock index d5853297..3ee734ed 100644 --- a/infra/migrations/uv.lock +++ b/infra/migrations/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.12" [[package]] @@ -11,76 +11,73 @@ dependencies = [ { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725 } +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893 }, + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[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 = "greenlet" version = "3.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220 }, - { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585 }, - { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215 }, - { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094 }, - { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358 }, - { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782 }, - { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475 }, - { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625 }, - { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791 }, - { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580 }, - { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060 }, - { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370 }, - { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987 }, - { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911 }, - { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911 }, - { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228 }, - { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689 }, - { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602 }, - { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683 }, - { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499 }, - { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579 }, - { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106 }, - { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478 }, - { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767 }, - { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800 }, - { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786 }, - { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518 }, - { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593 }, - { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800 }, - { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862 }, - { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637 }, - { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840 }, - { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416 }, - { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607 }, - { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397 }, - { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287 }, - { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469 }, - { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115 }, - { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246 }, - { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575 }, - { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428 }, - { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064 }, - { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962 }, - { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697 }, - { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047 }, - { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256 }, - { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956 }, - { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802 }, - { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853 }, - { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877 }, - { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333 }, - { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443 }, - { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998 }, - { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356 }, - { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470 }, - { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216 }, - { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427 }, - { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225 }, - { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590 }, + { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, + { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, + { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, + { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, + { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, + { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, + { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, + { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] [[package]] @@ -93,6 +90,12 @@ dependencies = [ { name = "python-dotenv" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.18.0" }, @@ -100,6 +103,21 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.2.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.0" }, + { name = "ruff", specifier = ">=0.15.0" }, +] + +[[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 = "mako" version = "1.3.12" @@ -107,72 +125,90 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219 } +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521 }, + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[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/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, + { 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 = "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]] @@ -183,9 +219,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799 } +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001 }, + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, ] [package.optional-dependencies] @@ -198,48 +234,98 @@ name = "psycopg-binary" version = "3.3.4" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122 }, - { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943 }, - { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697 }, - { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995 }, - { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180 }, - { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828 }, - { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757 }, - { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546 }, - { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197 }, - { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627 }, - { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782 }, - { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377 }, - { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023 }, - { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423 }, - { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137 }, - { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671 }, - { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601 }, - { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513 }, - { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243 }, - { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347 }, - { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393 }, - { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592 }, - { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292 }, - { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023 }, - { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985 }, - { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745 }, - { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486 }, - { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427 }, - { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549 }, - { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256 }, - { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204 }, - { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811 }, - { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849 }, + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[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.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] @@ -250,58 +336,58 @@ dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221 } +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681 }, - { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976 }, - { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937 }, - { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646 }, - { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695 }, - { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483 }, - { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494 }, - { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547 }, - { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782 }, - { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156 }, - { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832 }, - { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000 }, - { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641 }, - { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498 }, - { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807 }, - { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481 }, - { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565 }, - { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769 }, - { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319 }, - { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104 }, - { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356 }, - { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486 }, - { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479 }, - { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269 }, - { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260 }, - { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463 }, - { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204 }, - { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474 }, - { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567 }, - { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282 }, - { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406 }, - { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151 }, - { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178 }, - { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158 }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "tzdata" version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321 }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] diff --git a/infra/migrations/versions/0038_evolution_e1_runtime.py b/infra/migrations/versions/0038_evolution_e1_runtime.py new file mode 100644 index 00000000..1fcb669a --- /dev/null +++ b/infra/migrations/versions/0038_evolution_e1_runtime.py @@ -0,0 +1,56 @@ +"""Safely adapt legacy evolution rows to the E1 runtime schema.""" + +from __future__ import annotations + +from alembic import op + +from evolution_e1_candidate_sql import CANDIDATE_DOWN, CANDIDATE_UP +from evolution_e1_run_down_sql import RUN_DOWN +from evolution_e1_run_sql import RUN_UP + +revision: str = "0038" +down_revision: str | None = "0037" +branch_labels: str | tuple[str, ...] | None = None +depends_on: str | tuple[str, ...] | None = None + +_DOWNGRADE_GUARD = """ +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM strategy_evo_candidates + WHERE source_code IS NULL OR source_hash IS NULL + OR report IS NULL OR data_epoch IS NULL + ) THEN + RAISE EXCEPTION USING MESSAGE = + '0038 downgrade refused: candidates incompatible with the 0037 NOT NULL schema'; + END IF; + IF EXISTS ( + SELECT 1 FROM strategy_evo_runs + WHERE status IN ('queued', 'cancelling') OR started_at IS NULL + ) THEN + RAISE EXCEPTION USING MESSAGE = + '0038 downgrade refused: runs incompatible with the 0037 status/started_at schema'; + END IF; +END $$; +""" + + +def _execute_all(statements: tuple[str, ...]) -> None: + """Execute one migration phase at a time for inspectable failure boundaries.""" + for statement in statements: + op.execute(statement) + + +def upgrade() -> None: + """Backfill legacy truth first, then validate and install E1 constraints.""" + op.execute("SET LOCAL lock_timeout = '10s'") + _execute_all(RUN_UP) + _execute_all(CANDIDATE_UP) + + +def downgrade() -> None: + """Refuse incompatible rollback instead of deleting or falsifying business rows.""" + op.execute("SET LOCAL lock_timeout = '10s'") + op.execute(_DOWNGRADE_GUARD) + _execute_all(CANDIDATE_DOWN) + _execute_all(RUN_DOWN) diff --git a/infra/migrations/versions/0040_risk_locks_account_id.py b/infra/migrations/versions/0040_risk_locks_account_id.py new file mode 100644 index 00000000..9c6856db --- /dev/null +++ b/infra/migrations/versions/0040_risk_locks_account_id.py @@ -0,0 +1,47 @@ +"""risk_locks 补 account_id —— 风控锁按用户隔离(多租户上线必修) + +Revision ID: 0040 +Revises: 0038 +Create Date: 2026-08-21 + +背景:多用户上线后验证发现「风控日志跨账户泄露」——``risk_locks`` 表没有 account_id 列, +``/risk/locks`` / ``/risk/locks/history`` 只按 ``get_current_user`` 认证、不按账户过滤; +HTTP 路径的 ``RiskGuard.check`` 写锁也不带 account_id,导致 A 用户的 global/market/symbol +锁会拦到 B 用户的下单(跨账户误伤),且 B 用户能在风控面板看到 A 的锁。 + +``account_id TEXT``(可空,向后兼容老行;老行无归属,隔离后对任何账户不可见)。 +索引 ``(account_id, locked_at DESC)`` 覆盖「查本人最近 N 条锁」的热路径。 + +注:``down_revision`` 指向 0038(本提交时的链头)。0039(evolution ``llm_snapshot``) +为同分支并行 WIP、尚未合入;两者各自以 0038 为父,后并 main 者按项目约定修链 +(``down_revision`` 指向先合入的那一个),勿留双头。 +""" +from __future__ import annotations + +from alembic import op + +revision: str = "0040" +down_revision: str | None = "0038" +branch_labels: str | tuple[str, ...] | None = None +depends_on: str | tuple[str, ...] | None = None + + +def upgrade() -> None: + op.execute( + """ + ALTER TABLE risk_locks + ADD COLUMN IF NOT EXISTS account_id TEXT + """ + ) + op.execute( + "CREATE INDEX IF NOT EXISTS risk_locks_account_locked_idx " + "ON risk_locks (account_id, locked_at DESC) " + "WHERE account_id IS NOT NULL" + ) + + +def downgrade() -> None: + op.execute("DROP INDEX IF EXISTS risk_locks_account_locked_idx") + op.execute( + "ALTER TABLE risk_locks DROP COLUMN IF EXISTS account_id" + ) diff --git a/packages/orchestration/config/permissions.default.yaml b/packages/orchestration/config/permissions.default.yaml index 7c008f73..7ae908ae 100644 --- a/packages/orchestration/config/permissions.default.yaml +++ b/packages/orchestration/config/permissions.default.yaml @@ -40,6 +40,9 @@ allow: - "scheduler.create_job" - "scheduler.set_enabled" - "scheduler.trigger_job" + # 演化状态与候选详情只读 + - "evolver.get_evolution" + - "evolver.get_candidate" # Swarm 批量回测(ADR-0025):只读,无下单路径 - "swarm.*" @@ -87,6 +90,10 @@ ask: - "paper.deposit_cash" - "paper.reset_account" + # LLM 变异会产生费用;取消会改变运行状态,均需明确确认 + - "evolver.run_evolution" + - "evolver.abort_evolution" + deny: # 旧的"直接下单"路径全部禁——强制走 ADR-0012 plan/exec - "paper.submit_order" diff --git a/packages/orchestration/src/clients/evolver.ts b/packages/orchestration/src/clients/evolver.ts index a2183f34..66c4cbca 100644 --- a/packages/orchestration/src/clients/evolver.ts +++ b/packages/orchestration/src/clients/evolver.ts @@ -1,52 +1,66 @@ -/** - * services/evolver 客户端 —— 策略演化引擎。 - * - * 封装 evolver 服务(`POST /api/v1/runs` / `GET /api/v1/runs/{run_id}` / `GET /api/v1/candidates/{candidate_id}`)。 - */ +/** services/evolver 的 owner-scoped API 客户端。 */ +import { createHash, randomUUID } from "node:crypto"; + import { HttpClient } from "./http.js"; -/** 演化运行配置(与 evolver API schema 对齐)。 */ export type EvolutionConfig = { - universe?: string[]; - period_from?: string; - period_to?: string; - timeframe?: string; + venue: string; + symbol: string; + timeframe: string; + from_ts: string; + as_of: string; initial_cash?: number; + fee_rate?: number; + validation_split?: number; }; -/** 候选策略响应(与 evolver API CandidateResponse 对齐)。 */ export type CandidateResult = { candidate_id: string; run_id: string; + slot: number; generation: number; - parent_id: string | null; - source_code: string; - source_hash: string; + stage: string; + outcome: string; + source_code: string | null; + source_hash: string | null; + unified_diff: string | null; mutation_hint: string | null; + llm_cost_usd: number | null; fitness: number | null; - report: Record | null; + evaluation_snapshot: Record | null; + audit_snapshot: Record | null; + contract_snapshot: Record | null; + error_code: string | null; + error_message: string | null; overfitting_risk: string; - status: string; created_at: string | null; + updated_at: string | null; }; -/** 运行状态响应(与 evolver API RunStatusResponse 对齐)。 */ export type RunStatusResult = { run_id: string; seed_strategy_id: string; budget: number; - config: Record | null; - status: string; + config: Record; + status: "queued" | "running" | "cancelling" | "completed" | "failed" | "aborted"; + active_stage: string | null; llm_cost_usd: number; - candidates_count: number; - rejected_ast: number; - rejected_contract: number; - failed_eval: number; + queued_at: string; started_at: string | null; finished_at: string | null; + dataset_manifest: Record | null; + seed_report_snapshot: Record | null; + baseline_snapshot: Record | null; + failure_code: string | null; + failure_message: string | null; + attempted: number; + succeeded: number; + rejected: number; candidates: CandidateResult[]; }; +export type RunListResult = { items: RunStatusResult[]; next_cursor: string | null }; + export class EvolverClient { private readonly http: HttpClient; @@ -54,43 +68,41 @@ export class EvolverClient { this.http = new HttpClient(options); } - /** - * 启动一次演化运行。 - * - * @param budget 变异预算数(候选数量,默认 4) - * @param seedStrategyId 种子策略 ID(默认 "sma_cross_v1") - * @param config 演化配置(universe, period, timeframe, initial_cash) - * @returns 运行状态(含 run_id 用于后续轮询) - */ - async startRun( - budget?: number, - seedStrategyId?: string, - config?: EvolutionConfig, - ): Promise { - return await this.http.post("/api/v1/runs", { - budget: budget ?? 4, - seed_strategy_id: seedStrategyId ?? "sma_cross_v1", - config: config ?? undefined, + async startRun(options: { + budget?: number; + seedStrategyId?: string; + config: EvolutionConfig; + idempotencyKey?: string; + }): Promise { + const body = { + budget: options.budget ?? 4, + seed_strategy_id: options.seedStrategyId ?? "sma_cross_v1", + config: options.config, + }; + const key = options.idempotencyKey ?? operationKey(body); + return await this.http.post("/api/v1/runs", body, { + "Idempotency-Key": key, }); } - /** - * 查询演化运行状态。 - * - * @param runId 运行 UUID - * @returns 运行状态(含候选列表,按 fitness 降序) - */ + async listRuns(limit = 20): Promise { + return await this.http.get("/api/v1/runs", { limit }); + } + async getRun(runId: string): Promise { return await this.http.get(`/api/v1/runs/${runId}`); } - /** - * 查询单个候选策略详情。 - * - * @param candidateId 候选 UUID - * @returns 候选信息(含源码 + 报告 + fitness) - */ async getCandidate(candidateId: string): Promise { return await this.http.get(`/api/v1/candidates/${candidateId}`); } -} \ No newline at end of file + + async abortRun(runId: string): Promise { + return await this.http.post(`/api/v1/runs/${runId}/abort`, {}); + } +} + +function operationKey(body: unknown): string { + const digest = createHash("sha256").update(JSON.stringify(body)).digest("hex").slice(0, 24); + return `tool-${digest}-${randomUUID().slice(0, 8)}`; +} diff --git a/packages/orchestration/src/clients/http.ts b/packages/orchestration/src/clients/http.ts index 6a6da516..23e72832 100644 --- a/packages/orchestration/src/clients/http.ts +++ b/packages/orchestration/src/clients/http.ts @@ -56,9 +56,13 @@ export class HttpClient { return await this.request("GET", url); } - async post(path: string, body: unknown): Promise { + async post( + path: string, + body: unknown, + headers?: Record, + ): Promise { const url = this.buildUrl(path); - return await this.request("POST", url, body); + return await this.request("POST", url, body, headers); } private buildUrl( @@ -76,7 +80,12 @@ export class HttpClient { return url.toString(); } - private async request(method: string, url: string, body?: unknown): Promise { + private async request( + method: string, + url: string, + body?: unknown, + extraHeaders?: Record, + ): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); @@ -87,6 +96,7 @@ export class HttpClient { headers: { "Authorization": `Bearer ${this.token}`, ...(body !== undefined ? { "Content-Type": "application/json" } : {}), + ...extraHeaders, }, body: body !== undefined ? JSON.stringify(body) : undefined, signal: controller.signal, diff --git a/packages/orchestration/src/evals/runner.ts b/packages/orchestration/src/evals/runner.ts index d31b86b1..d4734114 100644 --- a/packages/orchestration/src/evals/runner.ts +++ b/packages/orchestration/src/evals/runner.ts @@ -1,6 +1,6 @@ import { createOrchestrator } from "../mastra/agents/create-orchestrator.js"; -import { AskApprovalCache } from "../permissions/ask-cache.js"; import { wireToolList } from "../mastra/tool-wiring.js"; +import { PendingApprovalsStore } from "../permissions/pending.js"; import { EvalFailureError } from "./errors.js"; import { createFixtureTools } from "./fixture-tools.js"; import { gradeTrial } from "./grader.js"; @@ -33,18 +33,10 @@ export async function runEvalTrial( if (!model) return failedResult(task, options, started, "live_provider", "live model missing"); const fixtureSet = createFixtureTools(task); - const askCache = new AskApprovalCache(1_000, () => undefined); - const pending = { - request: async () => ({ - decision: "deny" as const, - requestId: `eval-pending:${task.id}`, - via: "timeout" as const, - }), - }; + const pendingApprovals = new PendingApprovalsStore(() => undefined); const wired = wireToolList(fixtureSet.tools, { auditSink: () => undefined, - askCache, - pendingApprovals: pending, + pendingApprovals, askTimeoutMs: 100, }); const agent = createOrchestrator({ @@ -110,7 +102,7 @@ export async function runEvalTrial( return failedResult(task, options, started, classifyFailure(error), messageOf(error)); } finally { clearTimeout(timer); - askCache.clear(); + pendingApprovals.clearAll(); restoreFetch(); releaseNetwork(); } diff --git a/packages/orchestration/src/hooks/handlers/evolution-on-promote.ts b/packages/orchestration/src/hooks/handlers/evolution-on-promote.ts index 29d735fd..5a7baa1b 100644 --- a/packages/orchestration/src/hooks/handlers/evolution-on-promote.ts +++ b/packages/orchestration/src/hooks/handlers/evolution-on-promote.ts @@ -1,124 +1,5 @@ /** - * ``evolution-on-promote`` —— PostToolUse hook。 - * - * 在 ``paper.promote_candidate`` 执行成功后将候选源码作为种子自动触发 - * 一轮演化(E2),实现"promote → 自动演化下一代"的链式闭环。 - * - * 设计要点: - * - * - 非阻塞(``blocking: false``)—— promote 本身不因演化失败而回滚 - * - 异步 fire-and-forget —— hook handler 里 startRun,不 await 结果 - * (演化耗时数分钟,不应拖长 promote 的响应时间) - * - 每次 promote 触发 budget=2 的小规模探索(避免 budget 大 → LLM 费用高) - * - 演化结果落 evolver 内存表,用户可通过 evolver.get_evolution 查看 - * - * 坑: - * - evolver 服务不可用时静默失败(只 log warn,不抛错到 promote 调用者) - * - evolver 进程重启后内存表丢失 —— 演化结果只在 evolver 存活期间可查 - * - 若用户连续 promote 多个候选,每个都会触发一次演化,可能并行跑多个 + * 已停用:promote 只授权候选转正,不隐式授权额外 LLM 演化费用。 + * 演化必须由 `evolver.run_evolution` 经 permission `ask` 显式发起。 */ -import { getSettings } from "../../config.js"; -import { mintServiceToken } from "../../auth.js"; -import { EvolverClient } from "../../clients/evolver.js"; -import type { HookHandler, HookRegistration } from "../types.js"; - -export type EvolutionOnPromoteOptions = { - /** 每次 promote 触发的演化 budget(默认 2) */ - budget?: number; - /** 是否启用(默认 true);测试时可关闭 */ - enabled?: boolean; -}; - -/** - * 创建 promote → 演化 hook handler。 - * - * 在 promote 成功后,以 promoted 候选的源码为种子跑一轮演化。 - */ -export function createEvolutionOnPromoteHandler( - opts: EvolutionOnPromoteOptions = {}, -): HookHandler { - const budget = opts.budget ?? 2; - const enabled = opts.enabled ?? true; - - return async (ctx) => { - if (!enabled) return; - if (ctx.event !== "PostToolUse") return; - - // 只对成功的 promote 触发 - if (ctx.isError) return; - const output = ctx.toolOutput as Record | undefined; - if (!output) return; - - // 从 promote 返回提取 candidate_id - // paper.promote_candidate 返回 { id, status, ... } 或 { candidate_id, ... } - const candidateId = - (output.id as string | undefined) ?? - (output.candidate_id as string | undefined); - if (!candidateId) { - console.warn( - "[evolution-on-promote] promote response missing candidate_id, skipping evolution", - ); - return; - } - - // fire-and-forget:不阻塞 promote 的返回 - triggerEvolution(candidateId, budget).catch((err) => { - console.warn( - `[evolution-on-promote] failed to trigger evolution for ${candidateId}:`, - err, - ); - }); - }; -} - -/** - * 以 promoted 的候选为种子触发一次演化。 - * 异步执行,不阻塞 hook 调用者。 - */ -async function triggerEvolution( - candidateId: string, - budget: number, -): Promise { - const settings = getSettings(); - const token = await mintServiceToken({ - sub: "service:orchestration", - reason: "evolution-on-promote", - }); - - const client = new EvolverClient({ - baseUrl: settings.evolverServiceUrl, - token, - // 演化可能数分钟,用长超时 - timeoutMs: 600_000, - }); - - // 以 promoted 候选为种子,跑 budget 个变异 - // 传 seed_strategy_id = candidate: 让 evolver 能 trace 血缘 - const result = await client.startRun( - budget, - `candidate:${candidateId}`, - undefined, - ); - - console.info( - `[evolution-on-promote] evolution started for ${candidateId}: ` + - `run_id=${result.run_id}, budget=${budget}, status=${result.status}`, - ); -} - -/** - * 默认 promote → 演化 hook 注册项。 - */ -export function defaultEvolutionOnPromoteRegistration( - opts: EvolutionOnPromoteOptions = {}, -): HookRegistration { - return { - id: "evolution-on-promote", - event: "PostToolUse", - // 只监听 paper.promote_candidate 的成功返回 - matcher: "paper.promote_candidate", - handler: createEvolutionOnPromoteHandler(opts), - // 非阻塞 —— promote 不因演化失败而回滚 - blocking: false, - }; -} \ No newline at end of file +export {}; diff --git a/packages/orchestration/src/hooks/index.ts b/packages/orchestration/src/hooks/index.ts index 5cc74084..d954d4dd 100644 --- a/packages/orchestration/src/hooks/index.ts +++ b/packages/orchestration/src/hooks/index.ts @@ -12,7 +12,13 @@ export type { export { HookRunner } from "./runner.js"; export { toolMatches } from "./matcher.js"; -export { withHooks, defaultGetSessionId, AUTH_SUB_KEY } from "./with-hooks.js"; +export { + withHooks, + defaultGetAuthSub, + defaultGetSessionId, + defaultGetTurnId, + AUTH_SUB_KEY, +} from "./with-hooks.js"; export type { PermissionResolver, WithHooksOptions } from "./with-hooks.js"; export { createAuditLogHandler, defaultAuditRegistration } from "./handlers/audit-log.js"; @@ -70,11 +76,5 @@ export type { AnalystQuorumCheckOptions, } from "./handlers/analyst-quorum-check.js"; -export { - createEvolutionOnPromoteHandler, - defaultEvolutionOnPromoteRegistration, -} from "./handlers/evolution-on-promote.js"; -export type { EvolutionOnPromoteOptions } from "./handlers/evolution-on-promote.js"; - export { StopHookRunner, formatStopNotice } from "./stop-runner.js"; export type { StopDecision } from "./stop-runner.js"; diff --git a/packages/orchestration/src/hooks/with-hooks.ts b/packages/orchestration/src/hooks/with-hooks.ts index 4bb319a5..42ff4471 100644 --- a/packages/orchestration/src/hooks/with-hooks.ts +++ b/packages/orchestration/src/hooks/with-hooks.ts @@ -27,8 +27,10 @@ * - 现阶段不接 permission engine,仅留 ``permissionResolver`` 参数。task #3 接入。 */ import { projectApprovalInput } from "../permissions/approval-identity.js"; -import type { AskApprovalCache } from "../permissions/ask-cache.js"; -import type { PendingApprovalsStore } from "../permissions/pending.js"; +import { + type PendingApprovalsStore, + pendingApprovals as defaultPendingApprovals, +} from "../permissions/pending.js"; import type { HookRunner } from "./runner.js"; /** @@ -52,90 +54,50 @@ type GenericTool = { */ export const AUTH_SUB_KEY = "inalpha__authSub"; -/** Module-level flag:进程内仅 warn 一次 runId fallback / 完全失败,避免每次 tool 调用刷屏。 */ -let _warnedRunIdFallback = false; - /** - * 默认 sessionId 抽取器:按优先级从 Mastra runtime context 取**conversation 级** - * 稳定的 ID。 - * - * Mastra 1.10 实测 ``ToolExecutionContext`` 结构: - * - * - ``ctx.agent.threadId`` —— **整个对话线程**内稳定(这是想要的) - * - ``ctx.agent.resourceId`` —— per-user / per-resource,跨 thread 稳定 - * - ``ctx.runId`` —— **每个 user-turn 一个新值**(不稳定,跨 turn 会变) - * - 顶层 ``ctx.threadId`` 在 1.10 已**不再存在**(被移进 ``ctx.agent``) - * - * 因此优先级:``requestContext[AUTH_SUB_KEY]``(mastra server.middleware 从 Bearer 注入的 - * **已认证主体**,#91 多租户隔离的权威 scope)> ``ctx.agent.threadId`` > - * ``ctx.agent.resourceId`` > ``requestContext.sessionId`` > 顶层 ``sessionId`` > - * **undefined**(→ askCache 用稳定的 ``"__global__"`` scope)。 + * 提取 conversation 级稳定 ID;明确忽略每轮变化的 ``runId``。 * - * **故意不回退到 ``ctx.runId``**:runId 每 user-turn 一个新值,当 askCache key 会让 - * 跨 turn 审批永远 miss → 死循环(ADR-0018)。stable ``__global__`` 比 unstable runId - * 更对:跨 turn 能命中,approvalKey 里的 candidateId 仍区分不同候选。 - * - * 多租户隔离:dashboard 给每用户发各自 JWT(现发 CONSOLE_SUBJECT 常量)后,askSub 即按 - * 用户隔离,promote 审批不再跨用户越权——askCache 侧无需再改(#91)。 + * owner 由 ``defaultGetAuthSub`` 独立提取。缺少稳定 thread/session 时, + * costful ask 必须 fail closed,不能回退到 owner 或全局作用域。 */ export function defaultGetSessionId(ctx: unknown): string | undefined { if (!ctx || typeof ctx !== "object") return undefined; - const c = ctx as Record; - - // 最高优先级:mastra server.middleware 注入的已认证主体(#91 多租户 askCache scope)。 - // requestContext 是 Mastra RequestContext(Map → .get);也兼容自构造的普通对象。 - const rcForAuth = c.requestContext; - if (rcForAuth && typeof rcForAuth === "object") { - const getter = (rcForAuth as { get?: (k: string) => unknown }).get; - const authSub = - typeof getter === "function" - ? pickString(getter.call(rcForAuth, AUTH_SUB_KEY)) - : pickString((rcForAuth as Record)[AUTH_SUB_KEY]); - if (authSub) return authSub; - } - - // Mastra 1.10:threadId / resourceId 在 ctx.agent 下(thread-level 稳定) - const agent = c.agent; + const context = ctx as Record; + const agent = context.agent; if (agent && typeof agent === "object") { - const a = agent as Record; - const tid = pickString(a.threadId); - if (tid) return tid; - const rid = pickString(a.resourceId); - if (rid) return rid; + const threadId = pickString((agent as Record).threadId); + if (threadId) return threadId; } - - // 老 Mastra / 自构造 ctx fallback - const threadId = pickString(c.threadId); + const threadId = pickString(context.threadId); if (threadId) return threadId; - const rc = c.requestContext; - if (rc && typeof rc === "object") { - const sid = pickString((rc as Record).sessionId); - if (sid) return sid; + const requestContext = context.requestContext; + if (requestContext && typeof requestContext === "object") { + const sessionId = pickString((requestContext as Record).sessionId); + if (sessionId) return sessionId; + const getter = (requestContext as { get?: (key: string) => unknown }).get; + if (typeof getter === "function") { + const mappedSessionId = pickString(getter.call(requestContext, "sessionId")); + if (mappedSessionId) return mappedSessionId; + } } - const sid = pickString(c.sessionId); - if (sid) return sid; + return pickString(context.sessionId); +} - // 没有任何**会话级稳定** id(threadId / resourceId / sessionId)。 - // - // **故意不回退到 ctx.runId**:runId 每个 user-turn 一个新值,用它当 askCache key - // → 用户每次「同意」都是新 turn = 新 key → mark / consume 永远撞不上 → 审批死循环 - // (ADR-0018 ask-path)。返回 undefined 让 askCache 落**稳定**的 "__global__" key, - // 跨 turn 能命中(approvalKey 已含 candidateId 区分不同候选,60s TTL 兜底)。 - // - // 已知局限:AG-UI / Mastra 当前版本不把 memory.thread / resource 暴露进 tool - // ToolExecutionContext(ctx.agent.threadId / resourceId 槽位在、值恒空,实测)。 - // 单租户 dev 下 "__global__" 够用;接真实多租户前需让 threadId 真正进 ctx - // (改 dashboard ↔ mastra 转发或升级 @ag-ui/mastra),届时上面的 threadId 分支即生效。 - if (pickString(c.runId) && !_warnedRunIdFallback) { - _warnedRunIdFallback = true; - console.warn( - "[with-hooks] defaultGetSessionId: no stable thread/resource id in ctx; " + - "intentionally NOT using per-turn ctx.runId (would break cross-turn ask-cache). " + - "Using stable '__global__' ask-cache scope (single-tenant safe). " + - "Populate ctx.agent.threadId/resourceId for multi-tenant isolation.", - ); - } - return undefined; +/** Extracts the authenticated approval owner injected by identityMiddleware. */ +export function defaultGetAuthSub(ctx: unknown): string | undefined { + if (!ctx || typeof ctx !== "object") return undefined; + const requestContext = (ctx as Record).requestContext; + if (!requestContext || typeof requestContext !== "object") return undefined; + const getter = (requestContext as { get?: (key: string) => unknown }).get; + return typeof getter === "function" + ? pickString(getter.call(requestContext, AUTH_SUB_KEY)) + : pickString((requestContext as Record)[AUTH_SUB_KEY]); +} + +/** Extracts the per-turn run ID for diagnostics; approval never uses it. */ +export function defaultGetTurnId(ctx: unknown): string | undefined { + if (!ctx || typeof ctx !== "object") return undefined; + return pickString((ctx as Record).runId); } function pickString(v: unknown): string | undefined { @@ -152,29 +114,22 @@ export type PermissionResolver = ( input: unknown, ) => Promise<"allow" | "ask" | "deny"> | "allow" | "ask" | "deny"; -/** Eval 可注入的最小挂起审批接口,避免加载生产持久层。 */ -export type PendingApprovalsLike = Pick; - export type WithHooksOptions = { runner: HookRunner; /** 可选权限解析器(task #3 接入);缺省视为 allow。 */ permissionResolver?: PermissionResolver; - /** 可选 sessionId 提供器;从 tool ctx 中取(依赖 Mastra runtime context) */ + /** 可选 sessionId 提供器;必须返回稳定 thread/session ID。 */ getSessionId?: (toolCtx: unknown) => string | undefined; + /** 可选审批 owner 提供器;缺省读取已验签 Bearer 的 sub。 */ + getAuthSub?: (toolCtx: unknown) => string | undefined; /** * 可选挂起池(D-9.1b / ADR-0018)。permissionResolver=ask 时把请求挂进去,供 * CLI / Web 入口(GET /permissions/pending)查看。缺省用模块级单例(同 * ``mastra/index.ts`` 注册 HTTP routes 用的 store)。测试可注入 fresh 实例隔离。 */ - pendingApprovals?: PendingApprovalsLike; + pendingApprovals?: PendingApprovalsStore; /** ask 路径 store 超时毫秒数;缺省 30_000(30 秒)。0 / 负数视作默认。 */ askTimeoutMs?: number; - /** - * 可选 session-scoped 短期通行池(D-9.1b 修订)。让"第一次 ask → 用户在 chat - * 说允许 → agent 重调同 tool 同 input → 放行"在不引入 token 的前提下走通。 - * 缺省用模块级单例;测试可注入 fresh 实例。 - */ - askCache?: AskApprovalCache; }; /** @@ -191,6 +146,7 @@ export function withHooks(tool: T, opts: WithHooksOptions } const getSessionId = opts.getSessionId ?? defaultGetSessionId; + const getAuthSub = opts.getAuthSub ?? defaultGetAuthSub; const wrapped: GenericTool = { ...tool, @@ -201,6 +157,7 @@ export function withHooks(tool: T, opts: WithHooksOptions const toolName = tool.id; try { const sessionId = getSessionId(ctx); + const authSub = getAuthSub(ctx); // 1. PreToolUse const pre = await opts.runner.run("PreToolUse", { @@ -235,81 +192,51 @@ export function withHooks(tool: T, opts: WithHooksOptions } if (permDecision === "ask") { - // D-9.1b 修订 · session-scoped 短期通行池: - // - // 第一次 ask:cache 没命中 → mark + 返 requiresApproval;agent 在 chat - // 里向用户说明 + 等用户口头同意。 - // 第二次 ask(同 sessionId + 同 toolName + 同 input,60s 内):cache 命中 - // → 消费 + 继续走 execute;agent 无需在 input 里塞任何 token。 - // - // 这一改解决了 Mastra dev playground 没有 popup UI 时"用户在 chat 里说 - // '允许' / agent 重调还是被拦"的死循环。 - // - // 安全模型:agent 是否真等了用户回复完全靠 prompt 纪律 + 后端硬校验 - // (如 promote 的 fitness > baseline)作护栏;本缓存只解决 UX 死循环, - // 不是强制审批门。详见 permissions/ask-cache.ts。 - // - // 同时把请求挂进 PendingApprovalsStore(fire-and-forget)—— GET - // /permissions/pending 仍能列出来,为未来 CLI / Web 入口保留。 - const cache = - opts.askCache ?? - (await import("../permissions/ask-cache.js")).defaultAskCache; - const store = - opts.pendingApprovals ?? - (await import("../permissions/pending.js")).pendingApprovals; - const timeoutMs = - opts.askTimeoutMs && opts.askTimeoutMs > 0 ? opts.askTimeoutMs : undefined; - - // 审批匹配只用"身份字段"投影,不用完整 input:promote 等 tool 的 input 里 - // 带 LLM 自由生成的 reason,重调时措辞会变 → 用完整 input 当 key 会 cache - // miss → 反复弹确认。投影后只比对 candidateId 这类身份字段。 - // store / execute 仍用完整 effectiveInput(审计 & 落库要 reason)。 - // 详见 permissions/approval-identity.ts。 - const approvalKey = projectApprovalInput(toolName, effectiveInput); + const store = opts.pendingApprovals ?? defaultPendingApprovals; + const approvalInput = projectApprovalInput(toolName, effectiveInput); + if (!authSub || !sessionId) { + return { + isError: true, + deniedBy: "permission-ask", + requiresApproval: true, + toolName, + toolInput: effectiveInput, + message: + `APPROVAL_UNAVAILABLE: tool "${toolName}" cannot run because a verified owner ` + + `and stable thread/session ID are required. Do not retry or infer consent from chat text. ` + + `Explain this in the user's latest language.`, + }; + } if ( - cache.consume(sessionId, toolName, approvalKey, (msg) => - // stderr 走 mastra dev log,方便 user 实时看 mismatch 原因 - console.warn(`[askCache] ${msg}`), - ) + !store.consumeApproved({ + authSub, + sessionId, + toolName, + approvalInput, + }) ) { - // 第二次 ask 命中 cache → 一次性消费 + 放行(继续往下走 execute) - } else { - // 第一次 ask:mark cache + 挂 store(CLI 入口可见)+ 返 requiresApproval - cache.mark(sessionId, toolName, approvalKey); - void store.request({ + const pending = store.request({ + authSub, + sessionId, toolName, toolInput: effectiveInput, - sessionId, - authSub: sessionId, - timeoutMs, + approvalInput, + timeoutMs: + opts.askTimeoutMs && opts.askTimeoutMs > 0 ? opts.askTimeoutMs : undefined, }); return { isError: true, deniedBy: "permission-ask", requiresApproval: true, + requestId: pending.requestId, toolName, toolInput: effectiveInput, message: - `APPROVAL_REQUIRED: tool "${toolName}" needs explicit user consent.\n\n` + - `**There is NO UI button, NO popup, NO admin page.** The user can ONLY ` + - `reply with chat text. Never tell them to "click 允许" or "在界面上" / ` + - `"open the admin page" — they can't.\n\n` + - `Steps (reply in the user's language; match their latest message — never ` + - `hardcode Chinese or English):\n` + - ` 1. Translate the tool id into a plain user-meaningful phrase ` + - `(e.g. paper.promote_candidate → "add this strategy to the live pool" / ` + - `"把这条策略加入正式策略池"). Show the *why* + key inputs (use human ` + - `labels alongside IDs). Ask the user to confirm in chat.\n` + - ` 2. When the user replies with explicit consent ("ok / yes / allow / ` + - `允许 / 同意 / 好 / 上"), call this tool again with the **same input** ` + - `(no extra fields, no token; the retry just works).\n` + - ` 3. If they refuse / hesitate / give ambiguous answer → tell them ` + - `you've cancelled, do NOT retry.\n` + - ` 4. Never invent reasons like "the 60-second window timed out" / ` + - `"the system needs a popup" — if a retry returns this same APPROVAL_REQUIRED, ` + - `the most likely cause is that the LLM (you) changed the input between calls. ` + - `Re-explain to the user and ask again with the *exact* same args.`, + `APPROVAL_REQUIRED: tool "${toolName}" needs an explicit decision through the ` + + `trusted approval UI/API. Chat text, a new turn, or model output cannot approve it. ` + + `Show the purpose and key inputs, then wait. Reply in the user's latest language. ` + + `If denied or expired, cancel the action and do not retry automatically.`, }; } } diff --git a/packages/orchestration/src/mastra/agents/instructions/style.ts b/packages/orchestration/src/mastra/agents/instructions/style.ts index 911117e2..16cb5731 100644 --- a/packages/orchestration/src/mastra/agents/instructions/style.ts +++ b/packages/orchestration/src/mastra/agents/instructions/style.ts @@ -16,7 +16,11 @@ export const STYLE_AND_TERMS = ` 拉决策复盘,基于真实数据回答(如"还有没有优化空间"要落到它实际的决策 / 盈亏 / 风控拦截)。 - \`page=candidate_detail\` + \`candidate_id\` → 用户在某策略候选详情页。指代"这个策略 / 这个候选" 即指该 candidate:用 paper.get_candidate(candidateId) 拉源码 + metrics + fitness 后再答。 -- \`page=runners_list / lab_list / factors / risk / activity / overview\` → 只给大致语境、无具体实体; +- \`page=evolution_run_detail\` + \`evolution_run_id\` → 用户在某次 E1 演化运行详情页。指代 + "这次演化 / 这轮优化"时,用 evolver.get_evolution 拉最新状态、冻结数据与候选结果后再答。 +- \`page=evolution_candidate_detail\` + \`evolution_candidate_id\` → 用户在某个演化 slot 详情页。 + 指代"这个候选 / 这个改法"时,用 evolver.get_candidate 拉源码、diff、评估或拒绝原因后再答。 +- \`page=runners_list / lab_list / factors / risk / activity / evolution_list / divination / overview\` → 只给大致语境、无具体实体; 用户泛指时据此推断范围(如在 runners_list 问"哪个跑得最好"→ paper.list_strategy_runs)。 规则: diff --git a/packages/orchestration/src/mastra/agents/instructions/tool-catalog.ts b/packages/orchestration/src/mastra/agents/instructions/tool-catalog.ts index 840094e0..810e352e 100644 --- a/packages/orchestration/src/mastra/agents/instructions/tool-catalog.ts +++ b/packages/orchestration/src/mastra/agents/instructions/tool-catalog.ts @@ -152,18 +152,16 @@ export const TOOL_CATALOG = ` · 只支持 mode='tool' 的 job;mode='agent' 的 job 会返回 rejected(防递归) - scheduler.list_runs —— 列执行历史(用户问"X 最近跑成功没"/"scheduler 最近结果") -**策略演化(E2 · LLM 自动演算)**: -- evolver.run_evolution —— 启动自动演化轮次(LLM 驱动代码变异 → 三道沙盒 → 回测评估) - · 何时用:用户说"帮我演化 / 自动优化 / 变异一下 sma_cross"、"试试不同参数组合"、 - "自动帮我改进策略"、"看有没有比现在更好的策略" - · 返回:run_id + 候选列表(按 fitness 降序)+ 拒绝统计(rejected_ast / rejected_contract / failed_eval) - · 注意:每次演化 ≈ budget × (LLM 调用 + 沙盒 + 回测),budget=4 约 2-4 分钟。 - 种子策略默认 sma_cross_v1,要改标的 / 周期传 config。 - · 与 scheduler 结合:scheduler.create_job({ mode:'tool', payload:{ tool:'evolver.run_evolution', - input:{ budget:4 } } }) 可定时自动演化 - · 链式:promote 后会自动触发下一代演化(hook 自动,budget=2 小规模探索) -- evolver.get_evolution —— 查演化运行状态 + 候选列表 - · 何时用:run_evolution 返回后轮询拿结果,或用户问"上次演化结果" +**策略演化(E1 · 真实数据单代变异)**: +- evolver.run_evolution —— 显式启动一代 LLM 代码变异(源码审计 → 契约 → 同一冻结行情回测) + · 何时用:用户明确要求演化、优化或探索已存在策略的新方向 + · 必须传 venue / symbol / timeframe / from_ts / as_of;as_of 是真实当前时点 + · 返回 queued run_id;随后用 get_evolution 轮询。调用会产生费用,必须经 ask 确认 + · freshness 不足 fail closed;不会自动注册、promote、启动或在 promote 后隐式演化 + · 不要放进无人审批 scheduler:costful tool 没有人确认时不得执行 +- evolver.get_evolution —— 查 run、数据 manifest 与 slot 状态 +- evolver.get_candidate —— 查单个候选源码、diff、审计与回测快照 +- evolver.abort_evolution —— 用户明确要求时取消 active run,保留已完成 slot **沙盒计算(D-9 spike,ADR-0020 第二道运行隔离)**: - sandbox.run_code —— 跑 python / node 小段代码做一次性计算(默认 30s 超时,60s 内 allow,更长 ask) diff --git a/packages/orchestration/src/mastra/identity.ts b/packages/orchestration/src/mastra/identity.ts index b6e7256a..2b1873c1 100644 --- a/packages/orchestration/src/mastra/identity.ts +++ b/packages/orchestration/src/mastra/identity.ts @@ -46,27 +46,34 @@ export const identityMiddleware: MiddlewareHandler = async (c, next) => { // Invalid user configuration falls back to the configured model path. } - try { - const authz = c.req.header("Authorization"); - const token = authz?.startsWith("Bearer ") ? authz.slice(7).trim() : undefined; - if (token) { - const payload = await verifyToken(token); - const sub = typeof payload.sub === "string" && payload.sub ? payload.sub : undefined; - if (sub) { - const requestContext = c.get("requestContext") as { set?: (key: string, value: unknown) => void } | undefined; - if (typeof requestContext?.set === "function") { - requestContext.set(AUTH_SUB_KEY, sub); - } else if (!warnedNoRequestContext) { + const authz = c.req.header("Authorization"); + if (authz !== undefined) { + const match = /^Bearer\s+(.+)$/i.exec(authz.trim()); + if (!match?.[1]) { + return c.json({ error: "unauthorized" }, 401); + } + try { + const payload = await verifyToken(match[1].trim()); + const sub = typeof payload.sub === "string" && payload.sub.trim() ? payload.sub : undefined; + if (!sub) return c.json({ error: "unauthorized" }, 401); + const requestContext = c.get("requestContext") as + | { set?: (key: string, value: unknown) => void } + | undefined; + if (typeof requestContext?.set !== "function") { + if (!warnedNoRequestContext) { warnedNoRequestContext = true; - console.warn("[identity-mw] requestContext unavailable; authenticated scope was not injected"); + console.warn("[identity-mw] requestContext unavailable; rejecting authenticated request"); } + return c.json({ error: "identity_context_unavailable" }, 503); } - } - } catch (error) { - const code = (error as { code?: unknown } | null)?.code; - if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" && !warnedAuthSignature) { - warnedAuthSignature = true; - console.warn("[identity-mw] Bearer signature verification failed; check JWT_SECRET"); + requestContext.set(AUTH_SUB_KEY, sub); + } catch (error) { + const code = (error as { code?: unknown } | null)?.code; + if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" && !warnedAuthSignature) { + warnedAuthSignature = true; + console.warn("[identity-mw] Bearer signature verification failed; check JWT_SECRET"); + } + return c.json({ error: "unauthorized" }, 401); } } diff --git a/packages/orchestration/src/mastra/tool-wiring.ts b/packages/orchestration/src/mastra/tool-wiring.ts index 25a8bbd3..d60f2bc5 100644 --- a/packages/orchestration/src/mastra/tool-wiring.ts +++ b/packages/orchestration/src/mastra/tool-wiring.ts @@ -1,16 +1,15 @@ import { defaultAuditRegistration, } from "../hooks/handlers/audit-log.js"; -import { defaultEvolutionOnPromoteRegistration } from "../hooks/handlers/evolution-on-promote.js"; import { defaultFactorExpressionAuditRegistration } from "../hooks/handlers/factor-expression-audit.js"; import { defaultGridSizeCapRegistration } from "../hooks/handlers/grid-size-cap.js"; import { defaultIdempotencyRegistrations } from "../hooks/handlers/tool-idempotency.js"; import { defaultInjectCurrentDateRegistration } from "../hooks/handlers/inject-current-date.js"; import { defaultStrategyCodeAuditRegistration } from "../hooks/handlers/strategy-code-audit.js"; import { HookRunner } from "../hooks/runner.js"; -import { withHooks, type PendingApprovalsLike } from "../hooks/with-hooks.js"; -import type { AskApprovalCache } from "../permissions/ask-cache.js"; +import { withHooks } from "../hooks/with-hooks.js"; import { PermissionEngine } from "../permissions/engine.js"; +import type { PendingApprovalsStore } from "../permissions/pending.js"; import type { Decision } from "../permissions/types.js"; import { loadDefaultPermissions } from "../permissions/yaml_loader.js"; @@ -19,9 +18,8 @@ export type WireToolsOptions = { hookRunner?: HookRunner; permissionEngine?: PermissionEngine; auditSink?: (record: Record) => void; - pendingApprovals?: PendingApprovalsLike; + pendingApprovals?: PendingApprovalsStore; askTimeoutMs?: number; - askCache?: AskApprovalCache; }; /** hooks + permissions 包装后的宽松 tool 形态。 */ @@ -42,7 +40,6 @@ export function buildDefaultRunner( runner.register(defaultInjectCurrentDateRegistration()); runner.register(defaultStrategyCodeAuditRegistration()); runner.register(defaultFactorExpressionAuditRegistration()); - runner.register(defaultEvolutionOnPromoteRegistration()); const idempotency = defaultIdempotencyRegistrations(); runner.register(idempotency.pre); runner.register(idempotency.post); @@ -65,7 +62,6 @@ export function wireToolList( permissionResolver: resolver, pendingApprovals: options.pendingApprovals, askTimeoutMs: options.askTimeoutMs, - askCache: options.askCache, }), ); } diff --git a/packages/orchestration/src/permissions/api.ts b/packages/orchestration/src/permissions/api.ts index ace43631..3b8f28ec 100644 --- a/packages/orchestration/src/permissions/api.ts +++ b/packages/orchestration/src/permissions/api.ts @@ -25,21 +25,27 @@ interface ApiRouteSpec { handler: Handler; } +function getAuthSub(c: Context): string | undefined { + const requestContext = c.get("requestContext") as + | { get?: (key: string) => unknown } + | undefined; + const value = + typeof requestContext?.get === "function" ? requestContext.get(AUTH_SUB_KEY) : undefined; + return typeof value === "string" && value.length > 0 ? value : undefined; +} + const listPending: Handler = (c: Context) => { - return c.json({ pending: pendingApprovals.list() }); + const authSub = getAuthSub(c); + if (!authSub) return c.json({ error: "unauthorized" }, 401); + return c.json({ pending: pendingApprovals.list(authSub) }); }; const listApprovalHistory: Handler = async (c: Context) => { + const authSub = getAuthSub(c); + if (!authSub) return c.json({ error: "unauthorized" }, 401); const rawLimit = Number(c.req.query("limit") ?? "50"); const limit = Number.isFinite(rawLimit) ? rawLimit : 50; try { - // 从 requestContext 取已认证主体 sub(#91 身份注入 middleware 写入的)。 - // 有 sub → 按用户过滤;无 sub(本地 dev 无 Bearer token / token 无 sub)→ 返回全部。 - const rc = c.get("requestContext") as { get?: (k: string) => unknown } | undefined; - const authSub = - typeof rc?.get === "function" - ? (rc.get(AUTH_SUB_KEY) as string | undefined) - : undefined; return c.json({ history: await listHistory(authSub, limit) }); } catch (err) { console.error("[permissions] listHistory 失败:", err); @@ -69,7 +75,9 @@ const respondPending: Handler = async (c: Context) => { 400, ); } - const ok = pendingApprovals.respond(id, decision); + const authSub = getAuthSub(c); + if (!authSub) return c.json({ error: "unauthorized" }, 401); + const ok = pendingApprovals.respond(id, decision, authSub); if (!ok) { return c.json({ error: "not_found_or_expired", requestId: id }, 404); } diff --git a/packages/orchestration/src/permissions/approval-identity.ts b/packages/orchestration/src/permissions/approval-identity.ts index b590d0e7..2d187455 100644 --- a/packages/orchestration/src/permissions/approval-identity.ts +++ b/packages/orchestration/src/permissions/approval-identity.ts @@ -56,6 +56,8 @@ export const APPROVAL_IDENTITY_FIELDS: Readonly baseline`` - * 等硬校验作第二道防线;本缓存只解决 UX 死循环,不充当强制审批门 + * - **强制跨 user turn**:entry 记录首次 ask 的 ``ctx.runId``,只有后续不同 + * ``runId`` 才能消费。同一模型 turn 内立即重试只会再次得到审批要求,不能自批。 + * - 缺少 turn ID 时 fail closed:可记录待审批项,但不能消费。 */ interface AskCacheEntry { markedAt: number; + /** 首次触发 ask 的 user-turn ID;消费时必须来自另一个 turn。 */ + turnId: string | undefined; /** TTL 到期自动清理的 timer,consume 时记得 clearTimeout */ timer: ReturnType; } +export type AskConsumeResult = + | "consumed" + | "miss" + | "same_turn" + | "missing_turn_id"; + const DEFAULT_TTL_MS = 60_000; /** @@ -94,10 +101,15 @@ export class AskApprovalCache { } /** - * 标记 (sessionId, toolName, input) 已被 ask 过;TTL 后自动失效。 - * 若已存在条目,重置 TTL(连续多次 ask 同一动作不需要重启计时)。 + * 标记 (sessionId, toolName, input) 已被 ask 过,并记录当前 user turn。 + * 若已存在条目,重置 TTL,但同 turn 重试仍不能消费。 */ - mark(sessionId: string | undefined, toolName: string, input: unknown): void { + mark( + sessionId: string | undefined, + toolName: string, + input: unknown, + turnId: string | undefined, + ): void { this.maybeWarnGlobalFallback(sessionId, "mark", toolName); const key = AskApprovalCache.keyFor(sessionId, toolName, input); const existing = this.entries.get(key); @@ -112,29 +124,30 @@ export class AskApprovalCache { ts: new Date().toISOString(), }); }, this.ttlMs); - this.entries.set(key, { markedAt: Date.now(), timer }); + this.entries.set(key, { markedAt: Date.now(), turnId, timer }); this.telemetry({ event: "ask_marked", toolName, sessionId: sessionId ?? null, + hasTurnId: turnId !== undefined, ts: new Date().toISOString(), }); } /** - * 检查 + 一次性消费。命中(同 sessionId + toolName + input 60s 内被 mark)→ - * 删除条目 + 返 ``true``;未命中 → 返 ``false``。 + * 检查 + 一次性消费。只有 key 命中、TTL 有效且当前 ``turnId`` 与标记时不同, + * 才返回 ``consumed``;同 turn 自重试或无法验证 turn 边界都 fail closed。 * - * 未命中时若 ``debugSink`` 提供且存在"同 sessionId + 同 toolName 但 input 不同" - * 的条目,会调 sink 打 mismatch diff(帮 user 定位是 LLM 改了 input 哪个字段 - * 导致 cache 撞不上)。 + * input 未命中时若 ``debugSink`` 提供且存在同 session/tool 的其它条目,会输出 + * mismatch diff,帮助定位 LLM 改动了哪些字段。 */ consume( sessionId: string | undefined, toolName: string, input: unknown, + turnId: string | undefined, debugSink?: (msg: string) => void, - ): boolean { + ): AskConsumeResult { this.maybeWarnGlobalFallback(sessionId, "consume", toolName); const key = AskApprovalCache.keyFor(sessionId, toolName, input); const entry = this.entries.get(key); @@ -147,7 +160,7 @@ export class AskApprovalCache { reason: "no_entry", ts: new Date().toISOString(), }); - return false; + return "miss"; } const latency_ms = Date.now() - entry.markedAt; // 双保险:即便 setTimeout 未触发,超时仍按未命中处理 @@ -163,7 +176,41 @@ export class AskApprovalCache { latency_ms, ts: new Date().toISOString(), }); - return false; + return "miss"; + } + if (!entry.turnId || !turnId) { + if (debugSink) { + debugSink( + `AskCache: cannot verify a new user turn for ${toolName} (sid=${sessionId}); ` + + "refusing to consume approval", + ); + } + this.telemetry({ + event: "ask_consume_miss", + toolName, + sessionId: sessionId ?? null, + reason: "missing_turn_id", + latency_ms, + ts: new Date().toISOString(), + }); + return "missing_turn_id"; + } + if (entry.turnId === turnId) { + if (debugSink) { + debugSink( + `AskCache: same user turn retried ${toolName} (sid=${sessionId}); ` + + "waiting for a new user message", + ); + } + this.telemetry({ + event: "ask_consume_miss", + toolName, + sessionId: sessionId ?? null, + reason: "same_turn", + latency_ms, + ts: new Date().toISOString(), + }); + return "same_turn"; } clearTimeout(entry.timer); this.entries.delete(key); @@ -174,7 +221,7 @@ export class AskApprovalCache { latency_ms, ts: new Date().toISOString(), }); - return true; + return "consumed"; } /** 未命中时尝试找同 sid+tool 但 input 不同的条目,打 diff 帮排查。 */ diff --git a/packages/orchestration/src/permissions/defaults.ts b/packages/orchestration/src/permissions/defaults.ts index 297cb8ee..28e8c72f 100644 --- a/packages/orchestration/src/permissions/defaults.ts +++ b/packages/orchestration/src/permissions/defaults.ts @@ -43,6 +43,8 @@ export const DEFAULT_PERMISSIONS: PermissionConfig = { "scheduler.create_job", "scheduler.set_enabled", "scheduler.trigger_job", + "evolver.get_evolution", + "evolver.get_candidate", // Swarm 批量回测(ADR-0025):只读,无下单路径 "swarm.*", @@ -90,6 +92,8 @@ export const DEFAULT_PERMISSIONS: PermissionConfig = { // reset 是破坏性操作(删全部持仓行),后端另有 running-run 409 硬守门。 "paper.deposit_cash", "paper.reset_account", + "evolver.run_evolution", + "evolver.abort_evolution", ], deny: [ diff --git a/packages/orchestration/src/permissions/pending.ts b/packages/orchestration/src/permissions/pending.ts index 0e1c402f..64cb5f53 100644 --- a/packages/orchestration/src/permissions/pending.ts +++ b/packages/orchestration/src/permissions/pending.ts @@ -1,80 +1,49 @@ -/** - * ``PendingApprovalsStore`` —— in-memory ask 审批挂起池(D-9.1b / ADR-0018)。 - * - * ``permissionResolver`` 返 ``"ask"`` 时,``withHooks`` 不再直接 isError,而是: - * - * 1. 调 ``store.request({toolName, toolInput, sessionId})`` 注册挂起项 + 拿 promise - * 2. promise 等到前端 POST ``/permissions/{requestId}/respond`` 调 ``store.respond`` - * 3. 30s 超时(可配)→ 自动 deny + 从池中移除 - * 4. tool 拿到 decision:``"allow"`` → 真跑 execute;``"deny"`` → isError 提示 - * - * 设计取舍: - * - * - **进程内 Map**:单 mastra runtime 实例,重启即失效;多实例部署需要换 Redis - * 等共享存储(D-10+)。当前 dev / 单 instance prod 都够用 - * - **审计历史落 Postgres**(D-12 / migration 0021):每条审批全生命周期落 - * ``pending_approvals`` 表,dashboard 可回看终态;重启时启动 sweep 把遗留 - * pending 行置 ``expired_restart``。落库 fail-open —— 闸门永远是内存 Promise - * - **明确 fail-closed**:超时 / unknown 决策都按 deny 处理;用户关页面不会让 - * 挂起任务永远卡住 - * - **审计**:respond 时 log decision;超时自动 deny 也 log - * - * 引用:ADR-0018(askUserChoice)/ task D-9.1b。 - */ -import { randomUUID } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { insertPending, markResolved } from "./repo.js"; export type PendingDecision = "allow" | "deny"; -/** 前端可见的挂起项视图(不含 resolver 闭包)。 */ export interface PendingApprovalView { requestId: string; toolName: string; toolInput: unknown; - sessionId?: string; - createdAt: string; // ISO - deadline: string; // ISO -} - -interface PendingApprovalRecord extends PendingApprovalView { - resolve: (decision: PendingDecision) => void; - timer: ReturnType; + sessionId: string; + inputDigest: string; + createdAt: string; + deadline: string; } export interface PendingRequestArgs { toolName: string; toolInput: unknown; - sessionId?: string; - authSub?: string; + approvalInput: unknown; + sessionId: string; + authSub: string; timeoutMs?: number; } -export interface PendingRequestResult { - decision: PendingDecision; - requestId: string; - /** ``"user"`` = 前端响应;``"timeout"`` = 超时 deny。 */ - via: "user" | "timeout"; +export interface PendingConsumeArgs { + toolName: string; + approvalInput: unknown; + sessionId: string; + authSub: string; +} + +interface PendingApprovalRecord extends PendingApprovalView { + authSub: string; + status: "pending" | "approved"; + timer: ReturnType; } const DEFAULT_TIMEOUT_MS = 30_000; -/** - * Telemetry sink —— 默认 ``console.log(JSON.stringify(record))``,与 ``audit-log.ts`` - * + ``ask-cache.ts`` 同款 stdout-friendly 格式。测试可注入自定义 sink。 - */ export type PendingTelemetrySink = (record: Record) => void; -const defaultPendingTelemetrySink: PendingTelemetrySink = (r) => { - console.log(JSON.stringify(r)); +const defaultPendingTelemetrySink: PendingTelemetrySink = (record) => { + console.log(JSON.stringify(record)); }; -/** - * 审计持久层接口(D-12 / migration 0021)—— 实现见 ``repo.ts``。 - * - * **审计面不是闸门**:两个方法都必须自吞错误(fail-open),store 侧再兜一层 - * fire-and-forget;决策语义(fail-closed deny)完全由内存 Promise 保证。 - */ export interface ApprovalPersistence { insertPending(view: PendingApprovalView, authSub?: string): Promise; markResolved( @@ -84,11 +53,15 @@ export interface ApprovalPersistence { ): Promise; } -/** - * 进程内挂起池。``mastra/index.ts`` 在 runtime 启动时共享单例;测试可 new 独立实例。 - */ +/** Produces a deterministic SHA-256 digest for an approval-defining input. */ +export function approvalInputDigest(input: unknown): string { + return createHash("sha256").update(stableStringify(input)).digest("hex"); +} + +/** Stores pending and approved decisions until one matching tool call consumes them. */ export class PendingApprovalsStore { - private readonly pending = new Map(); + private readonly records = new Map(); + private readonly identityIndex = new Map(); private readonly telemetry: PendingTelemetrySink; private readonly persistence?: ApprovalPersistence; @@ -97,126 +70,186 @@ export class PendingApprovalsStore { this.persistence = persistence; } - /** fire-and-forget 落库 —— 审计面任何异常都不许影响审批流。 */ - private persist(fn: (p: ApprovalPersistence) => Promise): void { - if (!this.persistence) return; - try { - void fn(this.persistence).catch((err) => { - console.error("[pending] 审批审计落库失败(审批流不受影响):", err); - }); - } catch (err) { - console.error("[pending] 审批审计落库失败(审批流不受影响):", err); - } - } + /** Registers one owner-bound approval request, deduplicating identical active requests. */ + request(args: PendingRequestArgs): PendingApprovalView { + const identity = this.identityFor(args); + const existingId = this.identityIndex.get(identity); + const existing = existingId ? this.records.get(existingId) : undefined; + if (existing) return this.toView(existing); - /** - * 注册一个挂起审批,返 promise 等用户决策。 - * - * 超时(默认 30s)自动 deny,并从池中移除;调用方 await 拿到 ``{decision, requestId, via}``。 - */ - request(args: PendingRequestArgs): Promise { const requestId = randomUUID(); - const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const timeoutMs = args.timeoutMs && args.timeoutMs > 0 ? args.timeoutMs : DEFAULT_TIMEOUT_MS; const createdAt = new Date(); - const deadline = new Date(createdAt.getTime() + timeoutMs); - + const record: PendingApprovalRecord = { + requestId, + toolName: args.toolName, + toolInput: args.toolInput, + sessionId: args.sessionId, + authSub: args.authSub, + inputDigest: approvalInputDigest(args.approvalInput), + status: "pending", + createdAt: createdAt.toISOString(), + deadline: new Date(createdAt.getTime() + timeoutMs).toISOString(), + timer: setTimeout(() => this.expire(requestId), timeoutMs), + }; + this.records.set(requestId, record); + this.identityIndex.set(identity, requestId); this.telemetry({ event: "ask_pending_requested", requestId, toolName: args.toolName, - sessionId: args.sessionId ?? null, + sessionId: args.sessionId, + authSub: args.authSub, + inputDigest: record.inputDigest, timeoutMs, - ts: createdAt.toISOString(), + ts: record.createdAt, }); + this.persist((p) => p.insertPending(this.toView(record), args.authSub)); + return this.toView(record); + } - return new Promise((resolve) => { - const timer = setTimeout(() => { - const record = this.pending.get(requestId); - if (record) { - this.pending.delete(requestId); - this.telemetry({ - event: "ask_pending_resolved", - requestId, - toolName: args.toolName, - sessionId: args.sessionId ?? null, - decision: "deny", - via: "timeout", - latency_ms: timeoutMs, - ts: new Date().toISOString(), - }); - this.persist((p) => p.markResolved(requestId, "deny", "timeout")); - resolve({ decision: "deny", requestId, via: "timeout" }); - } - }, timeoutMs); - - const record: PendingApprovalRecord = { - requestId, - toolName: args.toolName, - toolInput: args.toolInput, - sessionId: args.sessionId, - createdAt: createdAt.toISOString(), - deadline: deadline.toISOString(), - timer, - resolve: (decision) => { - clearTimeout(timer); - this.pending.delete(requestId); - this.telemetry({ - event: "ask_pending_resolved", - requestId, - toolName: args.toolName, - sessionId: args.sessionId ?? null, - decision, - via: "user", - latency_ms: Date.now() - createdAt.getTime(), - ts: new Date().toISOString(), - }); - this.persist((p) => p.markResolved(requestId, decision, "user")); - resolve({ decision, requestId, via: "user" }); - }, - }; - this.pending.set(requestId, record); - const { resolve: _r, timer: _t, ...view } = record; - this.persist((p) => p.insertPending(view, args.authSub)); - }); + /** Lists only pending requests owned by the authenticated subject. */ + list(authSub: string): PendingApprovalView[] { + return Array.from(this.records.values()) + .filter((record) => record.authSub === authSub && record.status === "pending") + .map((record) => this.toView(record)); } - /** 前端 GET /permissions/pending —— 列出当前挂起项。 */ - list(): PendingApprovalView[] { - return Array.from(this.pending.values()).map( - ({ resolve: _r, timer: _t, ...view }) => view, - ); + /** Applies an explicit owner-authenticated decision; approved records remain consumable once. */ + respond(requestId: string, decision: PendingDecision, authSub: string): boolean { + const record = this.records.get(requestId); + if (!record || record.authSub !== authSub || record.status !== "pending") return false; + + this.telemetry({ + event: "ask_pending_resolved", + requestId, + toolName: record.toolName, + sessionId: record.sessionId, + authSub, + decision, + via: "user", + ts: new Date().toISOString(), + }); + this.persist((p) => p.markResolved(requestId, decision, "user")); + if (decision === "allow") { + record.status = "approved"; + } else { + this.remove(record); + } + return true; } - /** - * 前端 POST /permissions/{id}/respond —— 决策。 - * - * Returns ``true`` 表示决策被消费;``false`` 表示挂起项不存在 - * (已超时 / 已被消费 / 无效 id)—— 路由应回 404。 - */ - respond(requestId: string, decision: PendingDecision): boolean { - const record = this.pending.get(requestId); - if (!record) return false; - record.resolve(decision); + /** Atomically consumes one approved decision matching owner, thread, tool, and input digest. */ + consumeApproved(args: PendingConsumeArgs): boolean { + const identity = this.identityFor(args); + const requestId = this.identityIndex.get(identity); + const record = requestId ? this.records.get(requestId) : undefined; + if (!record || record.status !== "approved") return false; + if (Date.now() >= Date.parse(record.deadline)) { + this.expire(record.requestId); + return false; + } + this.remove(record); + this.telemetry({ + event: "ask_approval_consumed", + requestId: record.requestId, + toolName: record.toolName, + sessionId: record.sessionId, + authSub: record.authSub, + inputDigest: record.inputDigest, + ts: new Date().toISOString(), + }); return true; } - /** 当前挂起项数量(监控 / 测试用)。 */ + /** Returns the number of active pending or approved records. */ size(): number { - return this.pending.size; + return this.records.size; } - /** 清空所有挂起(测试用 / shutdown 时调)。所有挂起按 deny 解决。 */ + /** Revokes every active record during tests or shutdown. */ clearAll(reason: PendingDecision = "deny"): void { - for (const record of Array.from(this.pending.values())) { - record.resolve(reason); + for (const record of Array.from(this.records.values())) { + this.remove(record); + if (record.status === "pending") { + this.persist((p) => p.markResolved(record.requestId, reason, "user")); + } + } + } + + private expire(requestId: string): void { + const record = this.records.get(requestId); + if (!record) return; + this.remove(record); + this.telemetry({ + event: "ask_pending_resolved", + requestId, + toolName: record.toolName, + sessionId: record.sessionId, + authSub: record.authSub, + decision: "deny", + via: "timeout", + ts: new Date().toISOString(), + }); + if (record.status === "pending") { + this.persist((p) => p.markResolved(requestId, "deny", "timeout")); + } + } + + private remove(record: PendingApprovalRecord): void { + clearTimeout(record.timer); + this.records.delete(record.requestId); + this.identityIndex.delete( + this.identityKey(record.authSub, record.sessionId, record.toolName, record.inputDigest), + ); + } + + private identityFor(args: PendingConsumeArgs): string { + return this.identityKey( + args.authSub, + args.sessionId, + args.toolName, + approvalInputDigest(args.approvalInput), + ); + } + + private identityKey( + authSub: string, + sessionId: string, + toolName: string, + inputDigest: string, + ): string { + return `${authSub}\u0000${sessionId}\u0000${toolName}\u0000${inputDigest}`; + } + + private toView(record: PendingApprovalRecord): PendingApprovalView { + const { requestId, toolName, toolInput, sessionId, inputDigest, createdAt, deadline } = record; + return { requestId, toolName, toolInput, sessionId, inputDigest, createdAt, deadline }; + } + + private persist(fn: (persistence: ApprovalPersistence) => Promise): void { + if (!this.persistence) return; + try { + void fn(this.persistence).catch((error) => { + console.error("[pending] 审批审计落库失败(审批流不受影响):", error); + }); + } catch (error) { + console.error("[pending] 审批审计落库失败(审批流不受影响):", error); } } } -/** - * 进程内单例,由 ``withHooks`` 与 HTTP routes 共用。 - * 默认接 Postgres 审计持久层(repo.ts,fail-open);测试自建实例不传即纯内存。 - */ +function stableStringify(value: unknown): string { + if (value === undefined) return "undefined"; + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + const object = value as Record; + return `{${Object.keys(object) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`) + .join(",")}}`; +} + export const pendingApprovals = new PendingApprovalsStore(undefined, { insertPending, markResolved, diff --git a/packages/orchestration/src/tools/evolver-shared.ts b/packages/orchestration/src/tools/evolver-shared.ts new file mode 100644 index 00000000..03afaf49 --- /dev/null +++ b/packages/orchestration/src/tools/evolver-shared.ts @@ -0,0 +1,30 @@ +/** Evolver Mastra tools 的共享 schema 与客户端解析。 */ +import { z } from "zod"; + +import { resolveRequestToken } from "../auth.js"; +import { EvolverClient } from "../clients/evolver.js"; +import { getSettings } from "../config.js"; + +export type ToolRequestContext = { authToken?: string; get?: (key: string) => unknown }; + +export async function getEvolverClient(ctx?: ToolRequestContext): Promise { + return new EvolverClient({ + baseUrl: getSettings().evolverServiceUrl, + token: await resolveRequestToken(ctx), + timeoutMs: 30_000, + }); +} + +export const evolutionConfigSchema = z.object({ + venue: z.string().min(1).describe("数据 venue;按标的市场选择,不预设市场"), + symbol: z.string().min(1).describe("该 venue 使用的标的代码"), + timeframe: z.enum([ + "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", + "6h", "8h", "12h", "1d", "3d", "1wk", "1mo", + ]), + from_ts: z.string().datetime().describe("评估窗口起点,ISO-8601 UTC"), + as_of: z.string().datetime().describe("真实当前评估时点,ISO-8601 UTC"), + initial_cash: z.number().min(100).default(10_000), + fee_rate: z.number().min(0).max(0.1).default(0.001), + validation_split: z.number().min(0).max(0.5).default(0.3), +}); diff --git a/packages/orchestration/src/tools/evolver.ts b/packages/orchestration/src/tools/evolver.ts index e78468d7..a6275ceb 100644 --- a/packages/orchestration/src/tools/evolver.ts +++ b/packages/orchestration/src/tools/evolver.ts @@ -1,168 +1,66 @@ -/** - * services/evolver 的 Mastra tool 包装 —— 策略演化引擎。 - * - * 让 orchestrator agent 在对话里能启动演化轮次、查询演化状态。 - * 这是 E2 演化闭环的用户级入口。 - * - * Tool 设计遵循 docs/05-tool-skill-discipline.md 的"做什么 / 何时用 / 何时不用 / 坑"四要素。 - */ +/** Evolver Mastra tools:显式发起、查询、候选详情与取消。 */ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; -import { resolveRequestToken } from "../auth.js"; -import { EvolverClient } from "../clients/evolver.js"; -import { getSettings } from "../config.js"; - -type ToolRequestContext = { authToken?: string; get?: (key: string) => unknown }; - -async function getClient(ctx?: ToolRequestContext): Promise { - const settings = getSettings(); - const token = await resolveRequestToken(ctx); - return new EvolverClient({ - baseUrl: settings.evolverServiceUrl, - token, - // evolver 单次运行可能含 budget 个 LLM 调用 × budget 个回测,整轮可达数分钟 - timeoutMs: 600_000, - }); -} - -const EvolutionConfigSchema = z - .object({ - universe: z - .array(z.string().min(1)) - .optional() - .describe("标的列表,如 ['BTCUSDT']。默认 ['BTCUSDT']"), - period_from: z - .string() - .optional() - .describe("回测起始日期 YYYY-MM-DD。默认 2025-01-01"), - period_to: z - .string() - .optional() - .describe("回测截止日期 YYYY-MM-DD。默认 2025-12-31"), - timeframe: z - .enum(["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1wk", "1mo"]) - .optional() - .describe("K 线周期。默认 1h"), - initial_cash: z - .number() - .min(100) - .optional() - .describe("初始本金 USD。默认 10000"), - }) - .optional() - .describe("演化运行参数(可选,全部有默认值)"); - -// ──────────────────────────────────────────────────────────────────── -// evolver.run_evolution -// ──────────────────────────────────────────────────────────────────── +import { evolutionConfigSchema, getEvolverClient, type ToolRequestContext } from "./evolver-shared.js"; export const evolverRunEvolutionTool = createTool({ id: "evolver.run_evolution", description: ` - 启动一次策略演化运行(E2 演化引擎)。 - - 演化引擎会对种子策略做 N 次 LLM 驱动的代码变异,每版候选过三道沙盒 - (AST 审计 / 契约校验 / 回测评估),最终返回按 fitness 排序的候选列表。 - - 运行时覆盖维度(HintGenerator 循环输出): - - 入场上:成交量确认过滤器防假突破 - - 回撤上:跟踪止损(trailing stop) - - 参数级:快慢线周期网格寻优 - - 风控上:最大持仓天数 + 波动率过滤器 - - 何时用: - - 用户说"帮我演化策略 / 自动优化 / 变异一下 sma_cross" - - 用户说"试试不同参数组合 / 自动帮我改进策略" - - 手工迭代到上限后想要 LLM 探索新方向(互补:你走 pipeline 的 5 步迭代, - evolver 走并级变异探索) - - 用户说"看下有什么可能比现在更好的策略" - - 何时不用: - - 只想跑一次回测 → paper.run_backtest(evolver = 回测 × budget,成本高) - - 用户已明确要自己写代码 → paper.author_strategy - - 种子策略还没做过 baseline 评估 → 先跑一遍 paper.run_backtest 确认 baseline - 再说演化(不然变异方向没有参照系) - - 输出: - - 202 Accepted + run_id + status + 候选统计(candidates_count / rejected_ast / rejected_contract / failed_eval) - - 用 evolver.get_evolution 轮询最终结果(含候选列表按 fitness 降序) - - 坑: - - 每次演化 = budget × (LLM 调用 + 沙盒 + 回测),budget=4 约 2-4 分钟 - - 种子策略必须是 evolver 服务已知的 ID(默认 sma_cross_v1) - - 候选在 evolver 内存表里(E1 过渡方案,进程重启丢失);后续要 promote 的 - 走 paper.author_strategy 重新入库才持久化 - - config 默认 BTCUSDT 1h 2025 年,要改 universe / 周期必须显式传 - - Promote 成功后 evolver 会自动以 promoted 代码为种子触发生成下一代候选 - (链路:promote → evolver hook → 新一轮演化),这会花一些 budget +启动一次真实数据驱动的单代策略演化。每个候选经过源码审计、契约校验和同一冻结数据集回测;调用会产生 LLM 与计算成本,必须获得用户确认。 +何时用:用户明确要求变异、优化或探索已存在策略的新方向。 +何时不用:只需一次回测时用 paper.run_backtest;种子未验证或用户未授权额外成本时不要调用。 +坑:返回 queued run_id 后用 evolver.get_evolution 轮询;freshness 不足会 fail closed;不会自动 promote、注册或启动候选。 `.trim(), inputSchema: z.object({ - budget: z - .number() - .int() - .min(1) - .max(20) - .default(4) - .describe("变异预算数(产生多少候选),默认 4。最大值 20"), - seedStrategyId: z - .string() - .min(1) - .max(100) - .default("sma_cross_v1") - .describe("种子策略 ID,默认 sma_cross_v1。promote 后自动演化会填 candidate:"), - config: EvolutionConfigSchema, + budget: z.number().int().min(1).max(20).default(4), + seedStrategyId: z.string().min(1).max(128).default("sma_cross_v1"), + config: evolutionConfigSchema, + idempotencyKey: z.string().min(8).max(128).optional(), }), execute: async (inputData, ctx) => { - const tc = ctx?.requestContext as ToolRequestContext | undefined; - const client = await getClient(tc); - return await client.startRun( - inputData.budget ?? 4, - inputData.seedStrategyId ?? "sma_cross_v1", - inputData.config ?? undefined, - ); + const client = await getEvolverClient(ctx?.requestContext as ToolRequestContext | undefined); + return await client.startRun({ + budget: inputData.budget, + seedStrategyId: inputData.seedStrategyId, + config: inputData.config, + idempotencyKey: inputData.idempotencyKey, + }); }, }); -// ──────────────────────────────────────────────────────────────────── -// evolver.get_evolution -// ──────────────────────────────────────────────────────────────────── - export const evolverGetEvolutionTool = createTool({ id: "evolver.get_evolution", description: ` - 查询一次演化运行的完整状态(含候选列表,按 fitness 降序)。 - - 何时用: - - evolver.run_evolution 返回后轮询拿结果 - - 用户问"上次演化的结果怎么样 / 演化完了没" - - 何时不用: - - 只想看一个候选的详情 → evolver.get_candidate +查询演化 run 的状态、数据 manifest 和已完成 slot。 +何时用:轮询 queued/running/cancelling run,或复核 completed/failed/aborted 历史。 +何时不用:只看单个候选完整源码和 diff 时用 evolver.get_candidate。 +坑:跨用户资源统一返回 404;terminal 状态无需继续轮询。 + `.trim(), + inputSchema: z.object({ runId: z.string().uuid() }), + execute: async (inputData, ctx) => + await (await getEvolverClient(ctx?.requestContext as ToolRequestContext | undefined)).getRun(inputData.runId), +}); - 返回字段: - - status: "completed"(已完成)/ "running"(还在跑)/ "failed"(种子评估失败) - - candidates_count: 通过三道沙盒的候选数 - - candidates: 候选列表(完整源码 / fitness / report / mutation_hint) - - rejected_ast / rejected_contract / failed_eval: 各阶段的拒绝数 - - llm_cost_usd: 总 LLM 费用 +export const evolverGetCandidateTool = createTool({ + id: "evolver.get_candidate", + description: "查询当前用户拥有的单个演化候选源码、diff、审计与真实回测快照;不用它列 run。", + inputSchema: z.object({ candidateId: z.string().uuid() }), + execute: async (inputData, ctx) => + await (await getEvolverClient(ctx?.requestContext as ToolRequestContext | undefined)).getCandidate(inputData.candidateId), +}); - 坑: - - 候选在 evolver 内存中(E1 过渡),进程重启后查不到历史 - - 想要 promote 的候选——把它的 source_code 喂给 paper.author_strategy - 入库再 promote(evolver 本身不落策略表——那是 paper 的职责) - `.trim(), - inputSchema: z.object({ - runId: z - .string() - .min(1) - .describe("演化运行 UUID(从 evolver.run_evolution 的返回取 run_id)"), - }), - execute: async (inputData, ctx) => { - const tc = ctx?.requestContext as ToolRequestContext | undefined; - const client = await getClient(tc); - return await client.getRun(inputData.runId); - }, +export const evolverAbortEvolutionTool = createTool({ + id: "evolver.abort_evolution", + description: "取消 queued/running 演化 run;保留已完成 slot。仅在用户明确要求停止时使用,terminal run 无需调用。", + inputSchema: z.object({ runId: z.string().uuid() }), + execute: async (inputData, ctx) => + await (await getEvolverClient(ctx?.requestContext as ToolRequestContext | undefined)).abortRun(inputData.runId), }); -export const evolverTools = [evolverRunEvolutionTool, evolverGetEvolutionTool] as const; \ No newline at end of file +export const evolverTools = [ + evolverRunEvolutionTool, + evolverGetEvolutionTool, + evolverGetCandidateTool, + evolverAbortEvolutionTool, +] as const; diff --git a/packages/orchestration/src/tools/index.ts b/packages/orchestration/src/tools/index.ts index e0e2c380..aaa7efbb 100644 --- a/packages/orchestration/src/tools/index.ts +++ b/packages/orchestration/src/tools/index.ts @@ -101,6 +101,8 @@ import { tradePlanTools, } from "./trade-plan.js"; import { + evolverAbortEvolutionTool, + evolverGetCandidateTool, evolverGetEvolutionTool, evolverRunEvolutionTool, evolverTools, @@ -122,6 +124,10 @@ export { divinationCastHexagramTool, divinationDrawTarotTool, executeTradePlanTool, + evolverAbortEvolutionTool, + evolverGetCandidateTool, + evolverGetEvolutionTool, + evolverRunEvolutionTool, factorCatalogTool, factorEvaluateCandidateTool, factorListCandidatesTool, @@ -334,9 +340,11 @@ export const orchestratorToolList = [ // 玄学彩蛋(六爻 / 塔罗)—— 仅用户明确点名时召唤,输出禁入决策 divinationCastHexagramTool, divinationDrawTarotTool, - // E2 演化引擎(LLM 驱动策略变异 + 沙盒 + 评估) + // E1 演化引擎(显式单代变异 + 真实冻结数据评估) evolverRunEvolutionTool, evolverGetEvolutionTool, + evolverGetCandidateTool, + evolverAbortEvolutionTool, ] as const; /** 名字 → tool 索引,给 framework 路由用。 */ diff --git a/packages/orchestration/tests/approval-identity.test.ts b/packages/orchestration/tests/approval-identity.test.ts index 7158375e..95800331 100644 --- a/packages/orchestration/tests/approval-identity.test.ts +++ b/packages/orchestration/tests/approval-identity.test.ts @@ -52,6 +52,36 @@ describe("projectApprovalInput", () => { expect(first).not.toEqual(changedAllocation); }); + it("演化审批忽略幂等键,但绑定种子、预算与冻结数据配置", () => { + const base = { + seedStrategyId: "sma_cross_v1", + budget: 1, + config: { + venue: "binance", + symbol: "BTC/USDT", + timeframe: "1h", + from_ts: "2026-07-15T00:00:00Z", + as_of: "2026-08-14T00:00:00Z", + }, + }; + const first = projectApprovalInput("evolver.run_evolution", { + ...base, + idempotencyKey: "first-key", + }); + const retry = projectApprovalInput("evolver.run_evolution", { + ...base, + idempotencyKey: "retry-key", + }); + const largerBudget = projectApprovalInput("evolver.run_evolution", { + ...base, + budget: 2, + idempotencyKey: "first-key", + }); + + expect(first).toEqual(retry); + expect(first).not.toEqual(largerBudget); + }); + it("未登记 tool:原样返回完整 input", () => { const input = { foo: 1, bar: "z" }; expect(projectApprovalInput("some.other_tool", input)).toBe(input); diff --git a/packages/orchestration/tests/ask-path-e2e.test.ts b/packages/orchestration/tests/ask-path-e2e.test.ts index 155f16bb..81892984 100644 --- a/packages/orchestration/tests/ask-path-e2e.test.ts +++ b/packages/orchestration/tests/ask-path-e2e.test.ts @@ -1,394 +1,117 @@ -/** - * Ask-path 集成测试(ADR-0018 D-9.1b 修订)。 - * - * 与 `hooks.test.ts` 的单元拆分不同——本文件用**完整组合**(真 withHooks + 真 - * AskApprovalCache + 真 PendingApprovalsStore + 注入 telemetry sink)覆盖一次 - * ask 流程的所有可观察事件 + 跨 session 隔离 + TTL 自然过期。 - * - * 覆盖的事件序列(events emitted to telemetry sink): - * - * - 第一次 ask 拦截:``ask_marked``(cache.mark)+ ``ask_pending_requested``(store.request) - * - 第二次 ask 重调(cache hit):``ask_consumed`` - * - 用户拒绝 / cache 自然过期:``ask_cache_expired`` - * - store.request 30s timeout fire-and-forget:``ask_pending_resolved` via=timeout - * - 跨 sessionId 不复用 - * - * 不覆盖(已在 hooks.test.ts / permissions-pending.test.ts 单测): - * - matcher / runner 自身逻辑 - * - HTTP API endpoint(permissions-pending.test.ts §permissionsApiRoutes) - */ import { describe, expect, it, vi } from "vitest"; -import { defaultGetSessionId, HookRunner, withHooks } from "../src/hooks/index.js"; -import { AskApprovalCache } from "../src/permissions/ask-cache.js"; +import { AUTH_SUB_KEY, HookRunner, withHooks } from "../src/hooks/index.js"; import { PendingApprovalsStore } from "../src/permissions/pending.js"; -function makeEnv(opts: { cacheTtlMs?: number } = {}) { - const events: Array> = []; - const sink = (r: Record) => { - events.push(r); - }; - const cache = new AskApprovalCache(opts.cacheTtlMs ?? 60_000, sink); - const store = new PendingApprovalsStore(sink); - const runner = new HookRunner(); - return { events, cache, store, runner }; -} - -function eventsOf( - events: Array>, - name: string, -): Array> { - return events.filter((e) => e.event === name); +function toolContext(owner: string, thread: string, runId = "turn-1") { + const requestContext = new Map(); + requestContext.set(AUTH_SUB_KEY, owner); + return { requestContext, agent: { threadId: thread }, runId }; } -describe("ask-path e2e · 完整 happy path + telemetry 事件序列", () => { - it("第一次 ask 触发 mark + pending_requested;第二次重调 cache hit + consumed → execute", async () => { - const { events, cache, store, runner } = makeEnv(); - const exec = vi.fn().mockResolvedValue({ candidateId: "c-42", status: "promoted" }); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, +function makeApprovalTool(timeoutMs = 5_000) { + const events: Array> = []; + const store = new PendingApprovalsStore((event) => events.push(event)); + const execute = vi.fn().mockResolvedValue({ status: "executed" }); + const wrapped = withHooks( + { id: "paper.promote_candidate", execute }, + { + runner: new HookRunner(), permissionResolver: () => "ask", - askCache: cache, pendingApprovals: store, - getSessionId: () => "thread-A", - }); + askTimeoutMs: timeoutMs, + }, + ); + return { events, store, execute, wrapped }; +} - // 第一次:被拦 - const first = (await wrapped.execute!({ candidateId: "c-42" })) as { - isError: boolean; - requiresApproval: boolean; +describe("explicit approval path", () => { + it("pending、无关新 turn 和拒绝都不会执行", async () => { + const { store, execute, wrapped } = makeApprovalTool(); + const input = { candidateId: "c-42", reason: "first" }; + const first = (await wrapped.execute!(input, toolContext("user:alice", "thread-A"))) as { + requestId: string; }; - expect(first.isError).toBe(true); - expect(first.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - - // 第一次的 telemetry:先 mark 再 pending_requested(顺序由 with-hooks 决定) - expect(eventsOf(events, "ask_marked")).toHaveLength(1); - expect(eventsOf(events, "ask_pending_requested")).toHaveLength(1); - const marked = eventsOf(events, "ask_marked")[0]; - expect(marked).toMatchObject({ - toolName: "paper.promote_candidate", - sessionId: "thread-A", - }); - // 第二次:同 input → cache hit → 真 execute - const second = await wrapped.execute!({ candidateId: "c-42" }); - expect(exec).toHaveBeenCalledOnce(); - expect(second).toEqual({ candidateId: "c-42", status: "promoted" }); - - // 第二次的 telemetry:ask_consumed - expect(eventsOf(events, "ask_consumed")).toHaveLength(1); - const consumed = eventsOf(events, "ask_consumed")[0]; - expect(consumed).toMatchObject({ - toolName: "paper.promote_candidate", - sessionId: "thread-A", - }); - expect(typeof consumed.latency_ms).toBe("number"); - - // 副作用 cleanup:cache 一次性消费 - expect(cache.size()).toBe(0); + await wrapped.execute!(input, toolContext("user:alice", "thread-A", "turn-2")); + expect(execute).not.toHaveBeenCalled(); + expect(store.respond(first.requestId, "deny", "user:alice")).toBe(true); + await wrapped.execute!(input, toolContext("user:alice", "thread-A", "turn-3")); + expect(execute).not.toHaveBeenCalled(); store.clearAll(); }); -}); -describe("ask-path e2e · 审批身份投影(promote reason 措辞变化不重弹)", () => { - it("第一次带 reason-A,用户允许后重调换成 reason-B → 仍命中 cache → execute(不再弹第二次确认)", async () => { - const { events, cache, store, runner } = makeEnv(); - const exec = vi.fn().mockResolvedValue({ candidateId: "c-42", status: "promoted" }); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - getSessionId: () => "thread-A", - }); - - // 第一次:LLM 写了一段 reason → 被拦 - const first = (await wrapped.execute!({ - candidateId: "c-42", - reason: "2026-Q2 BTC 1h 回测,fitness=0.85 显著高于 baseline 0.32,max_drawdown=8%", - })) as { isError: boolean; requiresApproval: boolean }; - expect(first.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - - // 第二次:用户允许后 agent 重调,但 LLM 把 reason 换了个措辞(真实场景必现) - const second = await wrapped.execute!({ - candidateId: "c-42", - reason: "回测区间 2026 二季度 BTC 一小时线,适应度 0.85 远超基准 0.32,最大回撤约 8%", - }); - - // 关键断言:reason 不同也命中 cache → 真正 execute,而不是再返 requiresApproval - expect(exec).toHaveBeenCalledOnce(); - expect(second).toEqual({ candidateId: "c-42", status: "promoted" }); - expect(eventsOf(events, "ask_consumed")).toHaveLength(1); - // execute 拿到的仍是完整 input(含第二次的 reason)—— 投影只作用于 cache key - expect(exec).toHaveBeenCalledWith( - expect.objectContaining({ candidateId: "c-42", reason: expect.stringContaining("适应度") }), - undefined, + it("显式 approve 后只执行一次,二次重放失败", async () => { + const { store, execute, wrapped } = makeApprovalTool(); + const first = (await wrapped.execute!( + { candidateId: "c-42", reason: "first wording" }, + toolContext("user:alice", "thread-A"), + )) as { requestId: string }; + + expect(store.respond(first.requestId, "allow", "user:alice")).toBe(true); + const approved = await wrapped.execute!( + { candidateId: "c-42", reason: "changed audit wording" }, + toolContext("user:alice", "thread-A", "turn-2"), ); + expect(approved).toEqual({ status: "executed" }); + expect(execute).toHaveBeenCalledOnce(); + const replay = (await wrapped.execute!( + { candidateId: "c-42", reason: "third wording" }, + toolContext("user:alice", "thread-A", "turn-3"), + )) as { requiresApproval: boolean }; + expect(replay.requiresApproval).toBe(true); + expect(execute).toHaveBeenCalledOnce(); store.clearAll(); - cache.clear(); - }); - - it("候选 ID 不同时不串号:c-42 已允许不会放行 c-99", async () => { - const { cache, store, runner } = makeEnv(); - const exec = vi.fn().mockResolvedValue({ status: "promoted" }); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - getSessionId: () => "thread-A", - }); - - await wrapped.execute!({ candidateId: "c-42", reason: "x".repeat(20) }); - // 换一个候选重调 → 投影后 candidateId 不同 → 仍被拦(不复用 c-42 的许可) - const other = (await wrapped.execute!({ - candidateId: "c-99", - reason: "y".repeat(20), - })) as { requiresApproval: boolean }; - expect(other.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - - store.clearAll(); - cache.clear(); - }); -}); - -describe("ask-path e2e · 复合模拟盘启动只确认一次", () => { - it("首次 ask 后,确认重调即执行转正和 runner 启动,不再返回第二次 requiresApproval", async () => { - const { events, cache, store, runner } = makeEnv(); - const exec = vi.fn().mockResolvedValue({ - promotedNow: true, - candidate: { status: "promoted" }, - run: { status: "running" }, - }); - const tool = { id: "paper.promote_and_start_strategy", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - getSessionId: () => "thread-A", - }); - const input = { - candidateId: "c-42", - reason: "首次审计说明,包含回测与风控指标", - venue: "binance", - symbol: "BTC/USDT", - timeframe: "1h", - params: { fast: 10 }, - tradingMode: "spot", - leverage: 1, - allocation: 5000, - }; - - const first = (await wrapped.execute!(input)) as { requiresApproval: boolean }; - expect(first.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - - const second = await wrapped.execute!({ ...input, reason: "确认后改写审计文案,不改变运行范围" }); - expect(second).toMatchObject({ promotedNow: true, run: { status: "running" } }); - expect(exec).toHaveBeenCalledOnce(); - expect(eventsOf(events, "ask_marked")).toHaveLength(1); - expect(eventsOf(events, "ask_consumed")).toHaveLength(1); - - store.clearAll(); - cache.clear(); }); -}); - - -describe("ask-path e2e · 跨 sessionId 不复用", () => { - it("A 用户 mark 不被 B 用户 consume;两条独立 entry", async () => { - const { events, cache, store, runner } = makeEnv(); - const exec = vi.fn(); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - // sessionId 由 ctx 提供,每次 invoke 时 ctx.agent.threadId 不同 - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - }); - - // A 用户拦一次 - await wrapped.execute!({ candidateId: "c-42" }, { agent: { threadId: "thread-A" } }); - expect(cache.size()).toBe(1); - - // B 用户**同 input** 重调 → cache 不命中(不同 sessionId)→ 也被拦 - const bOut = (await wrapped.execute!( + it("不同 owner 或 thread 不能消费批准", async () => { + const { store, execute, wrapped } = makeApprovalTool(); + const first = (await wrapped.execute!( { candidateId: "c-42" }, - { agent: { threadId: "thread-B" } }, - )) as { isError: boolean; requiresApproval: boolean }; - expect(bOut.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - // 两条独立 entry:A + B 各一 - expect(cache.size()).toBe(2); + toolContext("user:alice", "thread-A"), + )) as { requestId: string }; + expect(store.respond(first.requestId, "allow", "user:bob")).toBe(false); + expect(store.respond(first.requestId, "allow", "user:alice")).toBe(true); - // telemetry:两次 ask_marked,sessionId 不同 - const marks = eventsOf(events, "ask_marked"); - expect(marks).toHaveLength(2); - const sids = new Set(marks.map((e) => e.sessionId)); - expect(sids).toEqual(new Set(["thread-A", "thread-B"])); + await wrapped.execute!({ candidateId: "c-42" }, toolContext("user:alice", "thread-B")); + await wrapped.execute!({ candidateId: "c-42" }, toolContext("user:bob", "thread-A")); + expect(execute).not.toHaveBeenCalled(); + await wrapped.execute!({ candidateId: "c-42" }, toolContext("user:alice", "thread-A")); + expect(execute).toHaveBeenCalledOnce(); store.clearAll(); - cache.clear(); }); -}); - -describe("ask-path e2e · cache TTL 自然过期", () => { - it("用户不重调时 cache TTL 到期触发 expired 事件", async () => { - const { events, cache, store, runner } = makeEnv({ cacheTtlMs: 50 }); - const exec = vi.fn(); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - getSessionId: () => "thread-A", - }); - - await wrapped.execute!({ candidateId: "c-42" }); - expect(cache.size()).toBe(1); - expect(eventsOf(events, "ask_marked")).toHaveLength(1); - - // 等 TTL 过期(用真 setTimeout,与项目其他测试一致) - await new Promise((r) => setTimeout(r, 80)); - - // cache 已被自动清理 - expect(cache.size()).toBe(0); - // telemetry:收到 ask_cache_expired - expect(eventsOf(events, "ask_cache_expired")).toHaveLength(1); - expect(eventsOf(events, "ask_cache_expired")[0]).toMatchObject({ - toolName: "paper.promote_candidate", - sessionId: "thread-A", - }); - - store.clearAll(); - }); -}); -describe("ask-path e2e · store.request fire-and-forget timeout", () => { - it("第一次 ask 后 30s timeout 触发 pending_resolved via=timeout(不影响 cache)", async () => { - const { events, cache, store, runner } = makeEnv(); - const exec = vi.fn(); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - askTimeoutMs: 30, // 用小超时让测试快 - getSessionId: () => "thread-A", - }); - - await wrapped.execute!({ candidateId: "c-42" }); - expect(eventsOf(events, "ask_pending_requested")).toHaveLength(1); - - // 等 store timeout 触发(与 cache TTL 独立) - await new Promise((r) => setTimeout(r, 60)); - - const resolved = eventsOf(events, "ask_pending_resolved"); - expect(resolved).toHaveLength(1); - expect(resolved[0]).toMatchObject({ - toolName: "paper.promote_candidate", - sessionId: "thread-A", - decision: "deny", - via: "timeout", - }); - // cache 仍存活(60s TTL 默认未到)—— 印证 store timeout 不影响 cache 路径 - expect(cache.size()).toBe(1); - - cache.clear(); - }); -}); - -describe("defaultGetSessionId · 不回退到 per-turn runId(dock 审批死循环根因)", () => { - it("ctx 只有 runId(无稳定 thread/resource)→ 返回 undefined(让 askCache 落稳定 __global__)", () => { - // 回归:AG-UI / Mastra dock 路径下 ctx.agent.threadId/resourceId 槽位在、值恒空, - // 只有每-turn 变的 runId。旧实现回退到 runId → 跨 turn askCache 必 miss → 审批死循环。 - expect(defaultGetSessionId({ runId: "turn-1" })).toBeUndefined(); - expect(defaultGetSessionId({ agent: { threadId: "", resourceId: "" }, runId: "turn-2" })).toBeUndefined(); - }); - - it("有稳定 id 时仍优先用(不破坏正常路径)", () => { - expect(defaultGetSessionId({ agent: { threadId: "thr-1" }, runId: "turn-1" })).toBe("thr-1"); - expect(defaultGetSessionId({ agent: { resourceId: "res-1" }, runId: "turn-1" })).toBe("res-1"); - expect(defaultGetSessionId({ requestContext: { sessionId: "sess-1" }, runId: "turn-1" })).toBe("sess-1"); - }); -}); - -describe("ask-path e2e · dock 场景回归(runId 每 turn 变,仍能跨 turn 审批)", () => { - it("无稳定 id、两次调用 runId 不同 → 第二次仍命中 __global__ → 放行(不再死循环)", async () => { - const { cache, store, runner } = makeEnv(); - const exec = vi.fn().mockResolvedValue({ candidateId: "c-7", status: "promoted" }); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - // 不传 getSessionId → 用真 defaultGetSessionId(被测路径) - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - }); - - // turn-1:用户问 promote → 被拦(mark __global__)。ctx 模拟 dock:只有 runId。 + it("timeout 撤销批准,后续调用不执行", async () => { + const { store, execute, wrapped } = makeApprovalTool(30); const first = (await wrapped.execute!( - { candidateId: "c-7", reason: "r".repeat(20) }, - { runId: "turn-1" }, - )) as { requiresApproval: boolean }; - expect(first.requiresApproval).toBe(true); - expect(exec).not.toHaveBeenCalled(); - - // turn-2:用户「同意」,agent 重调。**runId 变了**(dock 每 turn 新 runId)。 - // 旧实现:sid=turn-2 ≠ turn-1 → miss → 又弹确认(死循环)。 - // 修复后:两次 sid 都是 undefined → __global__ 稳定 → 命中 → execute。 - const second = await wrapped.execute!( - { candidateId: "c-7", reason: "different wording but same candidate" }, - { runId: "turn-2" }, - ); - expect(exec).toHaveBeenCalledOnce(); - expect(second).toEqual({ candidateId: "c-7", status: "promoted" }); + { candidateId: "c-42" }, + toolContext("user:alice", "thread-A"), + )) as { requestId: string }; + expect(store.respond(first.requestId, "allow", "user:alice")).toBe(true); + await new Promise((resolve) => setTimeout(resolve, 60)); + await wrapped.execute!({ candidateId: "c-42" }, toolContext("user:alice", "thread-A")); + expect(execute).not.toHaveBeenCalled(); store.clearAll(); - cache.clear(); }); -}); - -describe("ask-path e2e · 用户拒绝路径", () => { - it("agent 不重调 + cache 60s TTL 过期 → 后续相同 input 重新触发 ask(不复用旧 mark)", async () => { - const { events, cache, store, runner } = makeEnv({ cacheTtlMs: 50 }); - const exec = vi.fn(); - const tool = { id: "paper.promote_candidate", description: "", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - pendingApprovals: store, - getSessionId: () => "thread-A", - }); - // 第一次 ask - await wrapped.execute!({ candidateId: "c-42" }); - expect(eventsOf(events, "ask_marked")).toHaveLength(1); - - // 模拟"用户拒绝 → agent 不重调" —— 啥都不做,等 cache 过期 - await new Promise((r) => setTimeout(r, 80)); - expect(eventsOf(events, "ask_cache_expired")).toHaveLength(1); - expect(cache.size()).toBe(0); - - // 用户改变主意,agent 重新发起同样 tool 调用 → 应再走一遍 ask(不复用旧 mark) - await wrapped.execute!({ candidateId: "c-42" }); - expect(eventsOf(events, "ask_marked")).toHaveLength(2); // 新一轮 mark - expect(exec).not.toHaveBeenCalled(); + it("缺 owner 或稳定 thread 时 fail closed 且不创建全局审批", async () => { + const { store, execute, wrapped } = makeApprovalTool(); + const noOwner = (await wrapped.execute!( + { candidateId: "c-42" }, + { agent: { threadId: "thread-A" }, runId: "turn-1" }, + )) as { message: string }; + const requestContext = new Map([[AUTH_SUB_KEY, "user:alice"]]); + const noThread = (await wrapped.execute!( + { candidateId: "c-42" }, + { requestContext, runId: "turn-2" }, + )) as { message: string }; - store.clearAll(); - cache.clear(); + expect(noOwner.message).toContain("verified owner"); + expect(noThread.message).toContain("stable thread/session"); + expect(store.size()).toBe(0); + expect(execute).not.toHaveBeenCalled(); }); }); diff --git a/packages/orchestration/tests/hooks.test.ts b/packages/orchestration/tests/hooks.test.ts index f64ca685..3f15814e 100644 --- a/packages/orchestration/tests/hooks.test.ts +++ b/packages/orchestration/tests/hooks.test.ts @@ -14,7 +14,9 @@ import { createGridSizeCapHandler, createToolIdempotencyHandlers, defaultAuditRegistration, + defaultGetAuthSub, defaultGetSessionId, + defaultGetTurnId, defaultGridSizeCapRegistration, defaultIdempotencyRegistrations, toolMatches, @@ -318,162 +320,59 @@ describe("withHooks", () => { expect(out.deniedBy).toBe("permission"); }); - it("ask 第一次调 → 返 requiresApproval + 不调 execute (D-9.1b session cache)", async () => { + it("ask 仅在 owner 显式批准后执行一次", async () => { const { PendingApprovalsStore } = await import("../src/permissions/pending.js"); - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); const store = new PendingApprovalsStore(); - const cache = new AskApprovalCache(5_000); const runner = new HookRunner(); - const exec = vi.fn(); - const tool = { id: "test.tool", description: "", execute: exec }; - const wrapped = withHooks(tool, { + const exec = vi.fn().mockResolvedValue({ ran: true }); + const wrapped = withHooks({ id: "test.tool", execute: exec }, { runner, permissionResolver: () => "ask", pendingApprovals: store, - askCache: cache, + getAuthSub: () => "user:alice", + getSessionId: () => "thread-1", }); - const out = (await wrapped.execute!({ x: 1 })) as { - isError: boolean; - deniedBy: string; + const first = (await wrapped.execute!({ x: 1 })) as { + requestId: string; requiresApproval: boolean; - toolName: string; - toolInput: unknown; message: string; }; - - expect(exec).not.toHaveBeenCalled(); - expect(out.isError).toBe(true); - expect(out.deniedBy).toBe("permission-ask"); - expect(out.requiresApproval).toBe(true); - expect(out.toolName).toBe("test.tool"); - expect(out.toolInput).toEqual({ x: 1 }); - expect(out.message).toContain("test.tool"); - // message 不锁中文 / 英文(CLAUDE.md §3 用户语言原则) - expect(out.message).toMatch(/user's language|user language|their .* language/i); - // 第一次调后 cache 应该有标记 - expect(cache.size()).toBe(1); - store.clearAll(); - cache.clear(); - }); - - it("ask 第二次调(同 input)→ cache 命中 → 放行调 execute (session bypass)", async () => { - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); - const cache = new AskApprovalCache(5_000); - const runner = new HookRunner(); - const exec = vi.fn().mockResolvedValue({ ran: true }); - const tool = { id: "test.tool", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - }); - - // 第一次调 → 被 ask 拦 - await wrapped.execute!({ x: 1, y: "abc" }); + expect(first.requiresApproval).toBe(true); + expect(first.message).toMatch(/latest language/i); expect(exec).not.toHaveBeenCalled(); - expect(cache.size()).toBe(1); + expect(store.list("user:alice")).toHaveLength(1); - // 第二次调(同 input)→ cache 命中 → 放行 - const out = await wrapped.execute!({ x: 1, y: "abc" }); + expect(store.respond(first.requestId, "allow", "user:alice")).toBe(true); + expect(await wrapped.execute!({ x: 1 })).toEqual({ ran: true }); expect(exec).toHaveBeenCalledOnce(); - expect(exec).toHaveBeenCalledWith({ x: 1, y: "abc" }, undefined); - expect(out).toEqual({ ran: true }); - // 一次性消费:第二次后 cache 又空了 - expect(cache.size()).toBe(0); - }); - - it("ask 第二次调(input 改了)→ cache 不命中 → 再 ask 一次", async () => { - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); - const cache = new AskApprovalCache(5_000); - const runner = new HookRunner(); - const exec = vi.fn(); - const tool = { id: "test.tool", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - }); - - await wrapped.execute!({ x: 1 }); - expect(cache.size()).toBe(1); - - // 第二次 input 不一样 - const out = (await wrapped.execute!({ x: 999 })) as { deniedBy: string }; - expect(exec).not.toHaveBeenCalled(); - expect(out.deniedBy).toBe("permission-ask"); - // 又 mark 了一条新的(首次的还在)→ size=2 - expect(cache.size()).toBe(2); - }); - it("ask 第三次调(同 input)→ 仍要 ask(一次性消费 + 第二次已用掉)", async () => { - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); - const cache = new AskApprovalCache(5_000); - const runner = new HookRunner(); - const exec = vi.fn().mockResolvedValue({ ok: 1 }); - const tool = { id: "test.tool", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - }); - - await wrapped.execute!({ x: 1 }); // 第 1 次 → ask - await wrapped.execute!({ x: 1 }); // 第 2 次 → 放行 + 消费 + const replay = (await wrapped.execute!({ x: 1 })) as { requiresApproval: boolean }; + expect(replay.requiresApproval).toBe(true); expect(exec).toHaveBeenCalledOnce(); - - // 第 3 次:cache 空,又被 ask - const out = (await wrapped.execute!({ x: 1 })) as { deniedBy: string }; - expect(exec).toHaveBeenCalledOnce(); // 没增加 - expect(out.deniedBy).toBe("permission-ask"); + store.clearAll(); }); - it("ask 第一次调也把请求挂进 store (CLI / Web 备用入口)", async () => { - const { PendingApprovalsStore } = await import( - "../src/permissions/pending.js" - ); - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); + it("ask 批准绑定稳定 input digest", async () => { + const { PendingApprovalsStore } = await import("../src/permissions/pending.js"); const store = new PendingApprovalsStore(); - const cache = new AskApprovalCache(5_000); - const runner = new HookRunner(); - const tool = { id: "test.tool", execute: vi.fn() }; - const wrapped = withHooks(tool, { - runner, + const exec = vi.fn().mockResolvedValue({ ran: true }); + const wrapped = withHooks({ id: "test.tool", execute: exec }, { + runner: new HookRunner(), permissionResolver: () => "ask", pendingApprovals: store, - askCache: cache, - askTimeoutMs: 5_000, + getAuthSub: () => "user:alice", + getSessionId: () => "thread-1", }); - await wrapped.execute!({ y: 2 }); - expect(store.size()).toBe(1); - const pending = store.list()[0]!; - expect(pending.toolName).toBe("test.tool"); - expect(pending.toolInput).toEqual({ y: 2 }); - store.clearAll(); - }); + const first = (await wrapped.execute!({ x: 1 })) as { requestId: string }; + store.respond(first.requestId, "allow", "user:alice"); - it("ask cache 用 stable key:input key 顺序不同也匹中 (DeepSeek 实测痛点)", async () => { - const { AskApprovalCache } = await import("../src/permissions/ask-cache.js"); - const cache = new AskApprovalCache(5_000); - const runner = new HookRunner(); - const exec = vi.fn().mockResolvedValue({ ok: 1 }); - const tool = { id: "test.tool", execute: exec }; - const wrapped = withHooks(tool, { - runner, - permissionResolver: () => "ask", - askCache: cache, - }); - - // 第一次调:key 顺序 {candidateId, reason} - await wrapped.execute!({ candidateId: "X", reason: "alpha" }); + const changed = (await wrapped.execute!({ x: 2 })) as { requiresApproval: boolean }; + expect(changed.requiresApproval).toBe(true); expect(exec).not.toHaveBeenCalled(); - - // 第二次调:LLM 把 key 顺序换成 {reason, candidateId} —— stable stringify 仍命中 - const out = await wrapped.execute!({ reason: "alpha", candidateId: "X" }); - expect(exec).toHaveBeenCalledOnce(); - expect(out).toEqual({ ok: 1 }); - cache.clear(); + expect(await wrapped.execute!({ x: 1 })).toEqual({ ran: true }); + store.clearAll(); }); it("hook permissionOverride=allow wins over permissionResolver=deny", async () => { @@ -683,16 +582,32 @@ describe("audit-log handler", () => { // defaultGetSessionId · Mastra runtime context 字段抽取 // ──────────────────────────────────────────────────────────────────── +describe("defaultGetTurnId", () => { + it("只读取每个 user turn 唯一的 runId", () => { + expect(defaultGetTurnId({ runId: "turn-1" })).toBe("turn-1"); + expect(defaultGetTurnId({ runId: "" })).toBeUndefined(); + expect(defaultGetTurnId({ agent: { threadId: "thread-1" } })).toBeUndefined(); + expect(defaultGetTurnId(undefined)).toBeUndefined(); + }); +}); + +describe("defaultGetAuthSub", () => { + it("只读取 identity middleware 注入的已认证主体", () => { + const rc = new Map([[AUTH_SUB_KEY, "user-42"]]); + expect(defaultGetAuthSub({ requestContext: rc })).toBe("user-42"); + expect(defaultGetAuthSub({ requestContext: new Map() })).toBeUndefined(); + }); +}); + describe("defaultGetSessionId", () => { - it("prefers requestContext authSub — mastra middleware 注入的已认证主体 (#91 多租户 scope)", () => { - // RequestContext 是 Map(.get);authSub 高于 agent.threadId + it("owner 与稳定 thread 分别提取,不能互相替代", () => { const rc = new Map([[AUTH_SUB_KEY, "user-42"]]); - expect(defaultGetSessionId({ requestContext: rc, agent: { threadId: "t-1" } })).toBe( - "user-42", - ); + const ctx = { requestContext: rc, agent: { threadId: "t-1" } }; + expect(defaultGetAuthSub(ctx)).toBe("user-42"); + expect(defaultGetSessionId(ctx)).toBe("t-1"); }); - it("authSub 缺失 → 落回既有优先级(不影响无中间件的路径)", () => { + it("authSub 缺失不影响稳定 thread 提取", () => { const rc = new Map(); expect(defaultGetSessionId({ requestContext: rc, agent: { threadId: "t-1" } })).toBe("t-1"); }); @@ -701,9 +616,8 @@ describe("defaultGetSessionId", () => { expect(defaultGetSessionId({ threadId: "t-1", runId: "r-1" })).toBe("t-1"); }); - it("does NOT fall back to per-turn runId (dock ask-loop 根因;返回 undefined → askCache 稳定 __global__)", () => { - // runId 每 user-turn 变,用它当 askCache key 会让跨 turn 审批永远 miss → 死循环。 - // 修复后 getSessionId 故意忽略 runId,返回 undefined(askCache 落稳定 __global__)。 + it("does NOT fall back to per-turn runId", () => { + // runId 每 user-turn 变化,不能作为稳定审批 thread;缺失时必须 fail closed。 expect(defaultGetSessionId({ runId: "r-1" })).toBeUndefined(); }); diff --git a/packages/orchestration/tests/identity.test.ts b/packages/orchestration/tests/identity.test.ts index 034e2b7c..34079b16 100644 --- a/packages/orchestration/tests/identity.test.ts +++ b/packages/orchestration/tests/identity.test.ts @@ -63,13 +63,45 @@ describe("identityMiddleware", () => { expect(requestContext.get(AUTH_SUB_KEY)).toBe("user:alice"); }); - it("does not inject a subject for an invalid Bearer token", async () => { + it("returns 401 for an invalid Bearer token", async () => { const requestContext = new Map(); const response = await appWithRequestContext(requestContext).request("/", { headers: { Authorization: "Bearer invalid-token" }, }); + expect(response.status).toBe(401); + expect(requestContext.has(AUTH_SUB_KEY)).toBe(false); + }); + + it("rejects a verified Bearer when request context is unavailable", async () => { + const app = new Hono(); + app.use("*", identityMiddleware); + app.get("/", (context) => context.text("ok")); + + const response = await app.request("/", { + headers: { Authorization: `Bearer ${await mintServiceToken({ sub: "user:alice" })}` }, + }); + + expect(response.status).toBe(503); + }); + + it("keeps the unauthenticated dev path when no Bearer header is present", async () => { + const requestContext = new Map(); + const response = await appWithRequestContext(requestContext).request("/"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("ok"); + expect(requestContext.has(AUTH_SUB_KEY)).toBe(false); + }); + + it("returns 401 for an expired Bearer token", async () => { + const requestContext = new Map(); + const expired = await mintServiceToken({ sub: "user:alice" }, -1); + const response = await appWithRequestContext(requestContext).request("/", { + headers: { Authorization: `Bearer ${expired}` }, + }); + + expect(response.status).toBe(401); expect(requestContext.has(AUTH_SUB_KEY)).toBe(false); }); }); diff --git a/packages/orchestration/tests/permissions-pending.test.ts b/packages/orchestration/tests/permissions-pending.test.ts index d6b35eb4..477d88e3 100644 --- a/packages/orchestration/tests/permissions-pending.test.ts +++ b/packages/orchestration/tests/permissions-pending.test.ts @@ -1,226 +1,38 @@ -/** - * ``PendingApprovalsStore`` + permissions HTTP API 单测(D-9.1b / ADR-0018)。 - * - * 覆盖: - * - * - request → respond=allow → promise 解为 allow + 池清空 - * - request → respond=deny → promise 解为 deny - * - request → 超时 → 自动 deny + via=timeout - * - list 返回不含 resolver 闭包的视图 - * - respond 对未知 id 返 false - * - clearAll 全 deny + size=0 - * - API GET /permissions/pending 返列表 - * - API POST /permissions/:id/respond bad body / unknown id / 成功路径 - * - 审计持久化(D-12):request/respond/timeout 各落对应状态;persistence - * 抛错(sync / rejected promise)不影响审批流(fail-open) - */ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; +import { AUTH_SUB_KEY } from "../src/hooks/with-hooks.js"; import { permissionsApiRoutes } from "../src/permissions/api.js"; import { - type ApprovalPersistence, + approvalInputDigest, PendingApprovalsStore, - pendingApprovals as defaultPendingApprovals, + pendingApprovals, } from "../src/permissions/pending.js"; -// ──────────────────────────────────────────────────────────────────── -// PendingApprovalsStore -// ──────────────────────────────────────────────────────────────────── - -describe("PendingApprovalsStore.request → respond", () => { - it("allow → promise resolves with decision='allow' + size=0", async () => { - const store = new PendingApprovalsStore(); - const promise = store.request({ - toolName: "test.tool", - toolInput: { x: 1 }, - timeoutMs: 5_000, - }); - await vi.waitFor(() => expect(store.size()).toBe(1)); - const view = store.list()[0]!; - expect(view.toolName).toBe("test.tool"); - expect(view.toolInput).toEqual({ x: 1 }); - expect(store.respond(view.requestId, "allow")).toBe(true); - const result = await promise; - expect(result.decision).toBe("allow"); - expect(result.via).toBe("user"); - expect(result.requestId).toBe(view.requestId); - expect(store.size()).toBe(0); - }); - - it("deny → promise resolves with decision='deny'", async () => { - const store = new PendingApprovalsStore(); - const promise = store.request({ - toolName: "t", - toolInput: null, - timeoutMs: 5_000, - }); - await vi.waitFor(() => expect(store.size()).toBe(1)); - store.respond(store.list()[0]!.requestId, "deny"); - const result = await promise; - expect(result.decision).toBe("deny"); - expect(result.via).toBe("user"); - }); -}); - -describe("PendingApprovalsStore.request timeout", () => { - it("auto-denies after timeoutMs + via='timeout'", async () => { - const store = new PendingApprovalsStore(); - const result = await store.request({ - toolName: "t", - toolInput: null, - timeoutMs: 30, - }); - expect(result.decision).toBe("deny"); - expect(result.via).toBe("timeout"); - expect(store.size()).toBe(0); - }); -}); - -describe("PendingApprovalsStore misc", () => { - it("list view does not leak resolver/timer", () => { - const store = new PendingApprovalsStore(); - store.request({ toolName: "t", toolInput: null, timeoutMs: 10_000 }); - const view = store.list()[0]!; - expect(view).not.toHaveProperty("resolve"); - expect(view).not.toHaveProperty("timer"); - expect(view).toHaveProperty("requestId"); - expect(view).toHaveProperty("toolName"); - expect(view).toHaveProperty("createdAt"); - expect(view).toHaveProperty("deadline"); - store.clearAll(); - }); - - it("respond returns false for unknown requestId", () => { - const store = new PendingApprovalsStore(); - expect(store.respond("does-not-exist", "allow")).toBe(false); +function request(store: PendingApprovalsStore, owner = "user:alice", thread = "thread-A") { + return store.request({ + authSub: owner, + sessionId: thread, + toolName: "paper.promote_candidate", + toolInput: { candidateId: "c-42", reason: "audit" }, + approvalInput: { candidateId: "c-42" }, + timeoutMs: 5_000, }); - - it("clearAll resolves all as deny + size=0", async () => { - const store = new PendingApprovalsStore(); - const p1 = store.request({ toolName: "a", toolInput: null, timeoutMs: 5_000 }); - const p2 = store.request({ toolName: "b", toolInput: null, timeoutMs: 5_000 }); - await vi.waitFor(() => expect(store.size()).toBe(2)); - store.clearAll(); - expect(store.size()).toBe(0); - expect((await p1).decision).toBe("deny"); - expect((await p2).decision).toBe("deny"); - }); - - it("module-level singleton exists and is a PendingApprovalsStore", () => { - expect(defaultPendingApprovals).toBeInstanceOf(PendingApprovalsStore); - }); -}); - -// ──────────────────────────────────────────────────────────────────── -// 审计持久化(D-12 / migration 0020)—— mock persistence 注入 -// ──────────────────────────────────────────────────────────────────── - -function mockPersistence(): { - p: ApprovalPersistence; - inserts: ReturnType; - resolves: ReturnType; -} { - const inserts = vi.fn(async () => {}); - const resolves = vi.fn(async () => {}); - return { p: { insertPending: inserts, markResolved: resolves }, inserts, resolves }; } -describe("PendingApprovalsStore persistence (审计面)", () => { - it("request → insertPending 落 view;respond=allow → markResolved(allow, user)", async () => { - const { p, inserts, resolves } = mockPersistence(); - const store = new PendingApprovalsStore(() => {}, p); - const promise = store.request({ - toolName: "test.tool", - toolInput: { x: 1 }, - timeoutMs: 5_000, - }); - await vi.waitFor(() => expect(store.size()).toBe(1)); - const view = store.list()[0]!; - expect(inserts).toHaveBeenCalledTimes(1); - expect(inserts.mock.calls[0]![0]).toMatchObject({ - requestId: view.requestId, - toolName: "test.tool", - toolInput: { x: 1 }, - }); - - store.respond(view.requestId, "allow"); - await promise; - await vi.waitFor(() => expect(resolves).toHaveBeenCalledTimes(1)); - expect(resolves).toHaveBeenCalledWith(view.requestId, "allow", "user"); - }); - - it("timeout → markResolved(deny, timeout)", async () => { - const { p, resolves } = mockPersistence(); - const store = new PendingApprovalsStore(() => {}, p); - const result = await store.request({ - toolName: "t", - toolInput: null, - timeoutMs: 30, - }); - expect(result.via).toBe("timeout"); - await vi.waitFor(() => expect(resolves).toHaveBeenCalledTimes(1)); - expect(resolves).toHaveBeenCalledWith(result.requestId, "deny", "timeout"); - }); - - it("persistence 同步抛 / promise reject 都不影响审批流(fail-open)", async () => { - const throwing: ApprovalPersistence = { - insertPending: () => { - throw new Error("sync boom"); - }, - markResolved: async () => { - throw new Error("async boom"); - }, - }; - const store = new PendingApprovalsStore(() => {}, throwing); - const promise = store.request({ - toolName: "t", - toolInput: null, - timeoutMs: 5_000, - }); - await vi.waitFor(() => expect(store.size()).toBe(1)); - expect(store.respond(store.list()[0]!.requestId, "allow")).toBe(true); - const result = await promise; - expect(result.decision).toBe("allow"); - expect(store.size()).toBe(0); - }); - - it("不注 persistence 的实例零落库调用(纯内存,测试默认形态)", async () => { - const store = new PendingApprovalsStore(() => {}); - const result = await store.request({ - toolName: "t", - toolInput: null, - timeoutMs: 30, - }); - expect(result.decision).toBe("deny"); - }); -}); - -// ──────────────────────────────────────────────────────────────────── -// HTTP API(用 Hono 风格的 mock context;不拉真 server,直接调 handler) -// ──────────────────────────────────────────────────────────────────── - - -function fakeContext(opts: { - param?: Record; - body?: unknown; - jsonThrows?: boolean; -}) { - type JsonBody = { - status: number; - body: unknown; - }; +function fakeContext(owner: string | undefined, id: string, decision: "allow" | "deny") { const captured = { status: 200, body: null as unknown }; + const requestContext = new Map(); + if (owner) requestContext.set(AUTH_SUB_KEY, owner); return { captured, ctx: { + get: (key: string) => (key === "requestContext" ? requestContext : undefined), req: { - param: (key: string) => opts.param?.[key], - json: async () => { - if (opts.jsonThrows) throw new Error("invalid JSON"); - return opts.body; - }, + param: () => id, + json: async () => ({ decision }), + query: () => undefined, }, - json: (body: unknown, status = 200): JsonBody => { + json: (body: unknown, status = 200) => { captured.status = status; captured.body = body; return { status, body }; @@ -229,84 +41,110 @@ function fakeContext(opts: { }; } -describe("permissionsApiRoutes", () => { - const listRoute = permissionsApiRoutes.find( - (r) => r.path === "/permissions/pending" && r.method === "GET", - )!; - const respondRoute = permissionsApiRoutes.find( - (r) => r.path === "/permissions/:id/respond" && r.method === "POST", - )!; - - it("GET /permissions/pending returns list (default singleton)", async () => { - defaultPendingApprovals.clearAll(); - // 注:handler 自动用 module-level singleton(不接受注入),所以挂一个真请求 - void defaultPendingApprovals.request({ - toolName: "t.x", - toolInput: { a: 1 }, - timeoutMs: 30_000, - }); - await vi.waitFor(() => - expect(defaultPendingApprovals.size()).toBeGreaterThan(0), - ); - - const { ctx, captured } = fakeContext({}); - // biome-ignore lint: hono Context shape simplified for unit test - await listRoute.handler(ctx as any, async () => {}); - expect(captured.status).toBe(200); - const body = captured.body as { pending: unknown[] }; - expect(body.pending.length).toBeGreaterThan(0); - defaultPendingApprovals.clearAll(); - }); +describe("PendingApprovalsStore", () => { + it("binds approval to owner, thread, tool and deterministic input digest", () => { + const store = new PendingApprovalsStore(() => {}); + const view = request(store); + expect(view.inputDigest).toBe(approvalInputDigest({ candidateId: "c-42" })); + expect(store.list("user:bob")).toEqual([]); + expect(store.list("user:alice")).toHaveLength(1); + expect(store.respond(view.requestId, "allow", "user:bob")).toBe(false); + expect(store.respond(view.requestId, "allow", "user:alice")).toBe(true); + expect( + store.consumeApproved({ + authSub: "user:alice", + sessionId: "thread-B", + toolName: "paper.promote_candidate", + approvalInput: { candidateId: "c-42" }, + }), + ).toBe(false); + expect( + store.consumeApproved({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "paper.promote_candidate", + approvalInput: { candidateId: "c-42" }, + }), + ).toBe(true); + expect( + store.consumeApproved({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "paper.promote_candidate", + approvalInput: { candidateId: "c-42" }, + }), + ).toBe(false); + }); + + it("deny and timeout revoke the record", async () => { + const store = new PendingApprovalsStore(() => {}); + const denied = request(store); + expect(store.respond(denied.requestId, "deny", "user:alice")).toBe(true); + expect(store.size()).toBe(0); - it("POST /:id/respond rejects bad decision", async () => { - const { ctx, captured } = fakeContext({ - param: { id: "anything" }, - body: { decision: "maybe" }, + store.request({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "risk.update_config", + toolInput: {}, + approvalInput: {}, + timeoutMs: 20, }); - // biome-ignore lint: hono Context shape simplified for unit test - await respondRoute.handler(ctx as any, async () => {}); - expect(captured.status).toBe(400); + await new Promise((resolve) => setTimeout(resolve, 40)); + expect(store.size()).toBe(0); }); - it("POST /:id/respond returns 404 for unknown id", async () => { - defaultPendingApprovals.clearAll(); - const { ctx, captured } = fakeContext({ - param: { id: "no-such-request" }, - body: { decision: "allow" }, - }); - // biome-ignore lint: hono Context shape simplified for unit test - await respondRoute.handler(ctx as any, async () => {}); - expect(captured.status).toBe(404); + it("deduplicates repeated pending calls for the same approval identity", () => { + const store = new PendingApprovalsStore(() => {}); + expect(request(store).requestId).toBe(request(store).requestId); + expect(store.size()).toBe(1); + store.clearAll(); }); +}); - it("POST /:id/respond consumes the pending and resolves promise", async () => { - defaultPendingApprovals.clearAll(); - const promise = defaultPendingApprovals.request({ - toolName: "t", - toolInput: null, - timeoutMs: 5_000, - }); - await vi.waitFor(() => expect(defaultPendingApprovals.size()).toBe(1)); - const id = defaultPendingApprovals.list()[0]!.requestId; +describe("permissions approval HTTP API", () => { + const respondRoute = permissionsApiRoutes.find( + (route) => route.path === "/permissions/:id/respond" && route.method === "POST", + )!; - const { ctx, captured } = fakeContext({ - param: { id }, - body: { decision: "allow" }, + it("requires an authenticated owner", async () => { + pendingApprovals.clearAll(); + const view = pendingApprovals.request({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "paper.promote_candidate", + toolInput: { candidateId: "c-42" }, + approvalInput: { candidateId: "c-42" }, }); - // biome-ignore lint: hono Context shape simplified for unit test - await respondRoute.handler(ctx as any, async () => {}); - expect(captured.status).toBe(200); - expect((await promise).decision).toBe("allow"); - expect(defaultPendingApprovals.size()).toBe(0); - }); - - it("POST /:id/respond rejects non-JSON body", async () => { - const { ctx, captured } = fakeContext({ - param: { id: "x" }, - jsonThrows: true, + const { ctx, captured } = fakeContext(undefined, view.requestId, "allow"); + await respondRoute.handler(ctx as never, async () => {}); + expect(captured.status).toBe(401); + pendingApprovals.clearAll(); + }); + + it("writes explicit approve only for the matching owner", async () => { + pendingApprovals.clearAll(); + const view = pendingApprovals.request({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "paper.promote_candidate", + toolInput: { candidateId: "c-42" }, + approvalInput: { candidateId: "c-42" }, }); - // biome-ignore lint: hono Context shape simplified for unit test - await respondRoute.handler(ctx as any, async () => {}); - expect(captured.status).toBe(400); + const wrong = fakeContext("user:bob", view.requestId, "allow"); + await respondRoute.handler(wrong.ctx as never, async () => {}); + expect(wrong.captured.status).toBe(404); + + const matching = fakeContext("user:alice", view.requestId, "allow"); + await respondRoute.handler(matching.ctx as never, async () => {}); + expect(matching.captured.status).toBe(200); + expect( + pendingApprovals.consumeApproved({ + authSub: "user:alice", + sessionId: "thread-A", + toolName: "paper.promote_candidate", + approvalInput: { candidateId: "c-42" }, + }), + ).toBe(true); }); }); diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh index 7904a43f..0cd745f0 100755 --- a/scripts/check-consistency.sh +++ b/scripts/check-consistency.sh @@ -241,6 +241,29 @@ else ok "$ORCH_SRC 不存在(跳过)" fi +# ---------- C9: Evolver 开发与部署接线 ---------- +sect "C9 · Evolver 开发与部署接线" + +EVOLVER_CHECKS=( + "scripts/dev.sh|evolver:8005|dev.sh 管理 evolver:8005" + "infra/docker-compose.prod.yml|^ evolver:|prod Compose 声明 evolver service" + "infra/docker-compose.selfhost.yml|^ evolver:|selfhost Compose 覆盖 evolver" + "infra/docker-compose.prod.yml|EVOLVER_SERVICE_URL: http://evolver:8005|容器注入 Evolver URL" + ".github/workflows/ci.yml|service: \[data, paper, research, factor, evolver\]|CI lint 覆盖 evolver" + ".github/workflows/build-images.yml|inalpha-evolver:latest|CI 发布 evolver 镜像" +) +for check in "${EVOLVER_CHECKS[@]}"; do + IFS='|' read -r file pattern label <<< "$check" + if grep -qE "$pattern" "$file" 2>/dev/null; then + ok "$label" + else + fail "$label 缺失($file)" + fi +done +for file in infra/docker/Dockerfile.evolver services/evolver/uv.lock; do + [[ -f "$file" ]] && ok "$file 存在" || fail "$file 不存在" +done + # ---------- 总结 ---------- echo echo "====================" diff --git a/scripts/dev.sh b/scripts/dev.sh index 7aa0f26a..be088eb2 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -2,9 +2,9 @@ # dev.sh —— 一键起 Inalpha 本地 dev 环境 # # 用法: -# bash scripts/dev.sh [up] # 起 data + paper + research + orchestration(默认) +# bash scripts/dev.sh [up] # 起五个 Python service + orchestration(默认) # bash scripts/dev.sh stop # 停止所有由本脚本拉起的进程 -# bash scripts/dev.sh logs # 跟随四个 service 的日志 +# bash scripts/dev.sh logs # 跟随所有 service 的日志 # bash scripts/dev.sh status # 检查端口占用 + 已起进程的健康状态 # # 选项: @@ -16,6 +16,7 @@ # 8002 services/paper (FastAPI + uvicorn) GET /health # 8003 services/research (FastAPI + uvicorn) GET /health # 8004 services/factor (FastAPI + uvicorn) GET /health +# 8005 services/evolver (FastAPI + uvicorn) GET /health # 4111 mastra dev (默认端口) TCP connect # # 前置条件: @@ -23,6 +24,7 @@ # - services/data 需要可达的 Postgres + .env 配置 (DATABASE_URL / BINANCE_*) # - services/paper 在 D-8a 不强依赖 DB # - services/research 需要 LLM_API_KEY(默认 deepseek;LLM_PROVIDER=fake 时可空) +# - services/evolver 强制 DATABASE_URL,E1 只允许单 worker set -euo pipefail @@ -104,7 +106,7 @@ wait_tcp_open() { precheck_ports() { local conflict=0 - for entry in "data:8001" "paper:8002" "research:8003" "factor:8004" "orchestration:4111"; do + for entry in "data:8001" "paper:8002" "research:8003" "factor:8004" "evolver:8005" "orchestration:4111"; do local name="${entry%%:*}" local port="${entry##*:}" local owner @@ -156,10 +158,10 @@ start_service() { verify_ready() { # 等待所有 service 真正就绪。返回 0 = 全好;1 = 至少一个没起来 - # 超时各自不同: data 60s (Postgres + Binance lifespan 慢), paper 30s, research 30s, mastra 90s + # 超时各自不同:data 60s,其余 Python service 30s,mastra 90s local all_ok=1 echo "" - echo "[wait] 验证 service 就绪 (data 60s · paper 30s · research 30s · factor 30s · mastra 90s)..." + echo "[wait] 验证 service 就绪 (data 60s · paper/research/factor/evolver 30s · mastra 90s)..." if wait_http_ok "http://127.0.0.1:8001/health" 60; then echo " ✓ data http://127.0.0.1:8001/health" @@ -189,6 +191,13 @@ verify_ready() { all_ok=0 fi + if wait_http_ok "http://127.0.0.1:8005/health" 30; then + echo " ✓ evolver http://127.0.0.1:8005/health" + else + echo " ✗ evolver 未就绪 — 看 ${LOG_DIR}/evolver.log" + all_ok=0 + fi + if wait_tcp_open "127.0.0.1" 4111 90; then echo " ✓ mastra http://127.0.0.1:4111" else @@ -240,7 +249,7 @@ follow_logs() { status_report() { echo "=== 端口占用 ===" - for entry in "data:8001" "paper:8002" "research:8003" "factor:8004" "orchestration:4111"; do + for entry in "data:8001" "paper:8002" "research:8003" "factor:8004" "evolver:8005" "orchestration:4111"; do local name="${entry%%:*}" local port="${entry##*:}" local owner @@ -275,6 +284,11 @@ status_report() { else echo " ✗ factor not ready" fi + if curl -fsS -o /dev/null --max-time 1 "http://127.0.0.1:8005/health" 2>/dev/null; then + echo " ✓ evolver http://127.0.0.1:8005/health" + else + echo " ✗ evolver not ready" + fi if (echo > "/dev/tcp/127.0.0.1/4111") 2>/dev/null; then echo " ✓ mastra http://127.0.0.1:4111 (TCP open)" else @@ -299,6 +313,9 @@ case "$CMD" in start_service "factor" \ "${ROOT}/services/factor" \ "uv run uvicorn inalpha_factor.main:app --host 127.0.0.1 --port 8004 --reload" + start_service "evolver" \ + "${ROOT}/services/evolver" \ + "uv run uvicorn inalpha_evolver.main:app --host 127.0.0.1 --port 8005 --reload" start_service "orchestration" \ "${ROOT}/packages/orchestration" \ "pnpm dev" @@ -313,7 +330,7 @@ case "$CMD" in echo " 日志: bash scripts/dev.sh logs" echo " 状态: bash scripts/dev.sh status" echo " 停止: bash scripts/dev.sh stop" - echo " 端点: data=http://127.0.0.1:8001 paper=http://127.0.0.1:8002 research=http://127.0.0.1:8003 factor=http://127.0.0.1:8004 mastra=http://127.0.0.1:4111" + echo " 端点: data=http://127.0.0.1:8001 paper=http://127.0.0.1:8002 research=http://127.0.0.1:8003 factor=http://127.0.0.1:8004 evolver=http://127.0.0.1:8005 mastra=http://127.0.0.1:4111" else echo "" echo "⚠️ 至少一个 service 没起来。查日志: bash scripts/dev.sh logs" >&2 diff --git a/services/evolver/pyproject.toml b/services/evolver/pyproject.toml index 0a75dc83..f3165106 100644 --- a/services/evolver/pyproject.toml +++ b/services/evolver/pyproject.toml @@ -6,6 +6,8 @@ license = "AGPL-3.0-only" requires-python = ">=3.12" dependencies = [ "inalpha-paper", + "inalpha-shared", + "inalpha-shared-llm", "fastapi>=0.136.0", "uvicorn>=0.34.0", "pydantic>=2.13.0", @@ -23,6 +25,11 @@ dev = [ "mypy>=2.1.0", ] +[tool.uv.sources] +inalpha-paper = { path = "../paper", editable = true } +inalpha-shared = { path = "../_shared", editable = true } +inalpha-shared-llm = { path = "../_shared/llm", editable = true } + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/services/evolver/src/inalpha_evolver/api/cursor.py b/services/evolver/src/inalpha_evolver/api/cursor.py new file mode 100644 index 00000000..eb24870a --- /dev/null +++ b/services/evolver/src/inalpha_evolver/api/cursor.py @@ -0,0 +1,38 @@ +"""run keyset cursor 的编码与校验。""" +from __future__ import annotations + +import base64 +import json +from datetime import datetime +from uuid import UUID + +from fastapi import HTTPException + + +def encode_cursor(queued_at: datetime, run_id: UUID) -> str: + """编码不透明的 `(queued_at, run_id)` 游标。""" + payload = json.dumps( + [queued_at.isoformat(), str(run_id)], + separators=(",", ":"), + ).encode() + return base64.urlsafe_b64encode(payload).decode().rstrip("=") + + +def decode_cursor(value: str | None) -> tuple[datetime, UUID] | None: + """解码游标;格式错误统一返回 400。""" + if value is None: + return None + try: + padding = "=" * (-len(value) % 4) + queued_at, run_id = json.loads( + base64.urlsafe_b64decode(value + padding).decode() + ) + parsed_at = datetime.fromisoformat(queued_at.replace("Z", "+00:00")) + if parsed_at.tzinfo is None: + raise ValueError("cursor timestamp must include timezone") + return parsed_at, UUID(run_id) + except (ValueError, TypeError, json.JSONDecodeError) as exc: + raise HTTPException(status_code=400, detail="invalid cursor") from exc + + +__all__ = ["decode_cursor", "encode_cursor"] diff --git a/services/evolver/src/inalpha_evolver/api/detail_routes.py b/services/evolver/src/inalpha_evolver/api/detail_routes.py new file mode 100644 index 00000000..c1892c2a --- /dev/null +++ b/services/evolver/src/inalpha_evolver/api/detail_routes.py @@ -0,0 +1,63 @@ +"""Evolver run/candidate 查询与取消端点。""" + +from __future__ import annotations + +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Request +from inalpha_paper.account_id import account_id_from_user +from inalpha_shared.auth import User, get_current_user +from inalpha_shared.db import DBConn + +from ..storage import candidates, run_queries, runs +from .presenters import candidate_response, run_response +from .schemas import CandidateResponse, RunStatusResponse + +router = APIRouter() + + +@router.get("/runs/{run_id}", response_model=RunStatusResponse) +async def get_run( + run_id: UUID, + db: DBConn, + user: Annotated[User, Depends(get_current_user)], +) -> RunStatusResponse: + owner = account_id_from_user(user) + row = await runs.get_run(db, run_id, owner) + if row is None: + raise HTTPException(status_code=404, detail="run not found") + slots = await candidates.list_candidates(db, run_id, owner) + summary = await candidates.summarize(db, run_id) + return run_response(row, candidate_rows=slots, summary=summary) + + +@router.get("/candidates/{candidate_id}", response_model=CandidateResponse) +async def get_candidate( + candidate_id: UUID, + db: DBConn, + user: Annotated[User, Depends(get_current_user)], +) -> CandidateResponse: + row = await candidates.get_candidate(db, candidate_id, account_id_from_user(user)) + if row is None: + raise HTTPException(status_code=404, detail="candidate not found") + return candidate_response(row) + + +@router.post("/runs/{run_id}/abort", response_model=RunStatusResponse) +async def abort_run( + run_id: UUID, + request: Request, + db: DBConn, + user: Annotated[User, Depends(get_current_user)], +) -> RunStatusResponse: + owner = account_id_from_user(user) + transitioned = await run_queries.abort_owned(db, run_id, owner) + row = transitioned + if row is None: + row = await runs.get_run(db, run_id, owner) + if row is None: + raise HTTPException(status_code=404, detail="run not found") + if transitioned is not None and transitioned["status"] == "cancelling": + await request.app.state.evolution_manager.abort(run_id) + return run_response(row) diff --git a/services/evolver/src/inalpha_evolver/api/presenters.py b/services/evolver/src/inalpha_evolver/api/presenters.py new file mode 100644 index 00000000..c6de1d77 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/api/presenters.py @@ -0,0 +1,31 @@ +"""Evolver API 响应组装。""" +from __future__ import annotations + +from typing import Any + +from .schemas import CandidateResponse, RunStatusResponse + + +def candidate_response(row: dict[str, Any]) -> CandidateResponse: + return CandidateResponse.model_validate(row) + + +def run_response( + row: dict[str, Any], + *, + candidate_rows: list[dict[str, Any]] | None = None, + summary: dict[str, Any] | None = None, +) -> RunStatusResponse: + data = dict(row) + stats = summary or {} + data.update( + attempted=int(stats.get("attempted", 0)), + succeeded=int(stats.get("succeeded", 0)), + rejected=int(stats.get("rejected", 0)), + candidates=[candidate_response(item) for item in candidate_rows or []], + ) + data["llm_cost_usd"] = float(data.get("llm_cost_usd", 0)) + return RunStatusResponse.model_validate(data) + + +__all__ = ["candidate_response", "run_response"] diff --git a/services/evolver/src/inalpha_evolver/api/request_hash.py b/services/evolver/src/inalpha_evolver/api/request_hash.py new file mode 100644 index 00000000..325e93b9 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/api/request_hash.py @@ -0,0 +1,20 @@ +"""run 创建请求的规范化与幂等摘要。""" +from __future__ import annotations + +import hashlib +import json +from typing import Any + +from .schemas import StartRunRequest + + +def normalized_request(request: StartRunRequest) -> tuple[dict[str, Any], str]: + payload = request.model_dump(mode="json") + config = payload["config"] + digest = hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + return config, digest + + +__all__ = ["normalized_request"] diff --git a/services/evolver/src/inalpha_evolver/api/routes.py b/services/evolver/src/inalpha_evolver/api/routes.py index b2bbf980..af19e36d 100644 --- a/services/evolver/src/inalpha_evolver/api/routes.py +++ b/services/evolver/src/inalpha_evolver/api/routes.py @@ -1,105 +1,9 @@ -"""Evolver API 路由 —— 3 个端点。 +"""Evolver API 路由集合。""" +from fastapi import APIRouter -- ``POST /runs`` — 启动演化运行 -- ``GET /runs/{run_id}`` — 查询运行状态 + 候选列表 -- ``GET /candidates/{candidate_id}`` — 查询单个候选详情 -""" -from __future__ import annotations - -import logging -from uuid import UUID, uuid4 - -from fastapi import APIRouter, HTTPException, status -from fastapi.responses import JSONResponse - -from ..governor import run_one_generation -from ..mutator import Mutator -from .schemas import CandidateResponse, RunStatusResponse, StartRunRequest - -logger = logging.getLogger(__name__) +from .detail_routes import router as detail_router +from .run_routes import router as run_router router = APIRouter(prefix="/api/v1", tags=["evolution"]) - -# 内存存储(E1 过渡方案:E2 改为 DB 存储) -_runs: dict[UUID, RunStatusResponse] = {} -_candidates: dict[UUID, CandidateResponse] = {} - - -@router.get("/runs", response_model=list[RunStatusResponse]) -async def list_runs() -> list[RunStatusResponse]: - """列出所有演化运行(按 started_at 降序)。 - - E1 内存存储,返回全部。E2 改为 DB 分页查询。 - """ - return sorted( - _runs.values(), - key=lambda r: r.started_at, - reverse=True, - ) - - -@router.post("/runs", response_model=RunStatusResponse, status_code=status.HTTP_202_ACCEPTED) -async def start_run(request: StartRunRequest) -> RunStatusResponse: - """启动一次演化运行。 - - E1 用 MockMutator(不接真实 LLM API),返回手写 diff 验证闭环。 - E2 切换为真实 ``Mutator``(接 ``_shared/llm`` 的 Anthropic client)。 - - Body: - - seed_strategy_id: 种子策略 ID(默认 "sma_cross_v1") - - budget: 变异预算数(默认 4) - - config: 演化配置(universe, period, timeframe, initial_cash) - - Returns 202 Accepted + run_id 用于后续轮询。 - """ - config_dict = request.config.model_dump() if request.config else {} - run_id = uuid4() - - # 用真实 Mutator(走 GLM-5.2 / DeepSeek API) - mutator = Mutator() - - run = await run_one_generation( - run_id=run_id, - budget=request.budget, - config=config_dict, - mutator=mutator, - evaluator=None, - conn=None, - ) - - resp = RunStatusResponse( - run_id=run.run_id, - seed_strategy_id=request.seed_strategy_id, - budget=request.budget, - config=config_dict, - status=run.status, - llm_cost_usd=run.llm_cost_usd, - candidates_count=run.candidates_count, - rejected_ast=run.rejected_ast, - rejected_contract=run.rejected_contract, - failed_eval=run.failed_eval, - ) - _runs[run.run_id] = resp - return resp - - -@router.get("/runs/{run_id}", response_model=RunStatusResponse) -async def get_run(run_id: UUID) -> RunStatusResponse: - """查询演化运行状态。 - - Returns 200 + 运行状态 + 候选列表(按 fitness 降序)。 - """ - if run_id not in _runs: - raise HTTPException(status_code=404, detail="运行不存在") - return _runs[run_id] - - -@router.get("/candidates/{candidate_id}", response_model=CandidateResponse) -async def get_candidate(candidate_id: UUID) -> CandidateResponse: - """查询单个候选策略详情。 - - Returns 200 + 候选信息(含源码 + 报告 + fitness)。 - """ - if candidate_id not in _candidates: - raise HTTPException(status_code=404, detail="候选不存在") - return _candidates[candidate_id] \ No newline at end of file +router.include_router(run_router) +router.include_router(detail_router) diff --git a/services/evolver/src/inalpha_evolver/api/run_routes.py b/services/evolver/src/inalpha_evolver/api/run_routes.py new file mode 100644 index 00000000..f7e5c08d --- /dev/null +++ b/services/evolver/src/inalpha_evolver/api/run_routes.py @@ -0,0 +1,86 @@ +"""Evolver run 创建与列表端点。""" +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Annotated + +from fastapi import APIRouter, BackgroundTasks, Depends, Header, Request, status +from inalpha_paper.account_id import account_id_from_user +from inalpha_shared.auth import User, get_current_user +from inalpha_shared.db import DBConn +from inalpha_shared.errors import ConflictError, RateLimitedError + +from ..config import get_evolver_settings +from ..governor.seed_resolver import resolve_seed +from ..storage import run_queries, runs +from .cursor import decode_cursor, encode_cursor +from .presenters import run_response +from .request_hash import normalized_request +from .schemas import RunListResponse, RunStatusResponse, StartRunRequest + +router = APIRouter() + + +@router.post("/runs", response_model=RunStatusResponse, status_code=status.HTTP_202_ACCEPTED) +async def start_run( + body: StartRunRequest, + request: Request, + background: BackgroundTasks, + db: DBConn, + user: Annotated[User, Depends(get_current_user)], + idempotency_key: Annotated[str, Header(alias="Idempotency-Key", min_length=8, max_length=128)], +) -> RunStatusResponse: + owner = account_id_from_user(user) + settings = get_evolver_settings() + config, request_hash = normalized_request(body) + async with db.transaction(): + await db.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (str(owner),)) + active = await run_queries.count_active(db, owner) + seed = await resolve_seed(db, body.seed_strategy_id, owner) + row, created = await runs.insert_run( + db, + owner_account_id=owner, + requested_by_sub=user.user_id, + idempotency_key=idempotency_key, + request_hash=request_hash, + seed_strategy_id=seed.reference, + seed_source=seed.source_code, + seed_hash=seed.source_hash, + budget=body.budget, + config=config, + queued_at=datetime.now(UTC), + ) + if not created and row["request_hash"] != request_hash: + raise ConflictError("idempotency key reused", code="IDEMPOTENCY_KEY_REUSED") + if created and active >= settings.evolver_account_active_limit: + raise RateLimitedError("too many active evolution runs", code="EVOLUTION_RUN_LIMIT") + if created: + background.add_task(request.app.state.evolution_manager.notify_async) + return run_response(row) + + +@router.get("/runs", response_model=RunListResponse) +async def list_runs( + db: DBConn, + user: Annotated[User, Depends(get_current_user)], + limit: int = 20, + cursor: str | None = None, +) -> RunListResponse: + page_limit = min(max(limit, 1), 50) + rows = await run_queries.list_runs( + db, + account_id_from_user(user), + limit=page_limit + 1, + cursor=decode_cursor(cursor), + ) + has_more = len(rows) > page_limit + items = rows[:page_limit] + next_cursor = ( + encode_cursor(items[-1]["queued_at"], items[-1]["run_id"]) + if has_more and items + else None + ) + return RunListResponse( + items=[run_response(row, summary=row) for row in items], + next_cursor=next_cursor, + ) diff --git a/services/evolver/src/inalpha_evolver/api/schemas.py b/services/evolver/src/inalpha_evolver/api/schemas.py index 35ec1823..e4047253 100644 --- a/services/evolver/src/inalpha_evolver/api/schemas.py +++ b/services/evolver/src/inalpha_evolver/api/schemas.py @@ -1,61 +1,99 @@ -"""Evolver API schemas(Pydantic 模型)。""" +"""Evolver API 请求与响应模型。""" from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime, timedelta from typing import Any from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator + +from ..data.datetime_policy import MAX_AS_OF_CLOCK_SKEW +from ..data.manifest import DatasetManifest class EvolutionConfig(BaseModel): - """演化运行配置。""" + venue: str = Field(min_length=1, max_length=40) + symbol: str = Field(min_length=1, max_length=80) + timeframe: str = Field(pattern=r"^\d+(m|h|d|wk|mo)$") + from_ts: datetime + as_of: datetime + initial_cash: float = Field(default=10_000.0, ge=100) + fee_rate: float = Field(default=0.001, ge=0, le=0.1) + validation_split: float = Field(default=0.3, ge=0, le=0.5) - universe: list[str] = Field(default_factory=lambda: ["BTCUSDT"]) - period_from: str = "2025-01-01" - period_to: str = "2025-12-31" - timeframe: str = "1h" - initial_cash: float = 10000.0 + @field_validator("from_ts", "as_of") + @classmethod + def normalize_datetimes(cls, value: datetime, info: ValidationInfo) -> datetime: + """拒绝无时区 cutoff,其余时间统一为 UTC。""" + if value.tzinfo is None: + if info.field_name == "as_of": + raise ValueError("as_of must be timezone-aware") + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) + @model_validator(mode="after") + def validate_window(self) -> EvolutionConfig: + if self.as_of > datetime.now(UTC) + MAX_AS_OF_CLOCK_SKEW: + raise ValueError("as_of exceeds trusted current time") + if self.from_ts >= self.as_of: + raise ValueError("from_ts must be earlier than as_of") + if self.as_of - self.from_ts > timedelta(days=3650): + raise ValueError("evolution window cannot exceed 10 years") + return self -class StartRunRequest(BaseModel): - """启动演化运行的请求体。""" - seed_strategy_id: str = "sma_cross_v1" - budget: int = Field(default=4, ge=1, le=100) - config: EvolutionConfig = Field(default_factory=EvolutionConfig) +class StartRunRequest(BaseModel): + seed_strategy_id: str = Field(default="sma_cross_v1", max_length=128) + budget: int = Field(default=4, ge=1, le=20) + config: EvolutionConfig class CandidateResponse(BaseModel): - """候选策略的响应模型。""" - candidate_id: UUID run_id: UUID + slot: int generation: int - parent_id: UUID | None = None - source_code: str - source_hash: str + stage: str + outcome: str + source_code: str | None = None + source_hash: str | None = None + unified_diff: str | None = None mutation_hint: str | None = None + llm_cost_usd: float | None = None fitness: float | None = None - report: dict[str, Any] | None = None + evaluation_snapshot: dict[str, Any] | None = None + audit_snapshot: dict[str, Any] | None = None + contract_snapshot: dict[str, Any] | None = None + error_code: str | None = None + error_message: str | None = None overfitting_risk: str = "high" - status: str = "evaluated" + data_epoch: int = 0 created_at: datetime | None = None + updated_at: datetime | None = None class RunStatusResponse(BaseModel): - """演化运行状态响应。""" - run_id: UUID seed_strategy_id: str budget: int - config: dict[str, Any] | None = None + config: dict[str, Any] status: str + active_stage: str | None = None llm_cost_usd: float = 0.0 - candidates_count: int = 0 - rejected_ast: int = 0 - rejected_contract: int = 0 - failed_eval: int = 0 + queued_at: datetime started_at: datetime | None = None finished_at: datetime | None = None - candidates: list[CandidateResponse] = Field(default_factory=list) \ No newline at end of file + dataset_manifest: DatasetManifest | None = None + seed_report_snapshot: dict[str, Any] | None = None + baseline_snapshot: dict[str, Any] | None = None + failure_code: str | None = None + failure_message: str | None = None + attempted: int = 0 + succeeded: int = 0 + rejected: int = 0 + candidates: list[CandidateResponse] = Field(default_factory=list) + + +class RunListResponse(BaseModel): + items: list[RunStatusResponse] + next_cursor: str | None = None diff --git a/services/evolver/src/inalpha_evolver/config.py b/services/evolver/src/inalpha_evolver/config.py index 48365000..c31cbe5b 100644 --- a/services/evolver/src/inalpha_evolver/config.py +++ b/services/evolver/src/inalpha_evolver/config.py @@ -36,6 +36,14 @@ class EvolverSettings(BaseSettings): description="单个回测任务的超时秒数(传参给 subprocess_runner)。", ) + evolver_run_timeout_s: int = Field( + default=1200, + alias="EVOLVER_RUN_TIMEOUT_S", + ge=60, + le=14400, + description="整个单代 run 的总墙钟期限。", + ) + evolver_job_mem_gb: int = Field( default=2, alias="EVOLVER_JOB_MEM_GB", @@ -43,6 +51,25 @@ class EvolverSettings(BaseSettings): le=8, description="单个回测子进程的内存上限(GB)。", ) + evolver_max_running_runs: int = Field( + default=1, + alias="EVOLVER_MAX_RUNNING_RUNS", + ge=1, + le=8, + ) + evolver_account_active_limit: int = Field( + default=2, + alias="EVOLVER_ACCOUNT_ACTIVE_LIMIT", + ge=1, + le=20, + ) + data_service_url: str = Field( + default="http://127.0.0.1:8001", + alias="DATA_SERVICE_URL", + ) + jwt_secret: str = Field(default="dev-secret", alias="JWT_SECRET") + jwt_algorithm: str = Field(default="HS256", alias="JWT_ALGORITHM") + service_token_ttl_s: int = Field(default=3600, ge=60, le=86400) # ---- LLM ---- llm_api_key: str = Field( @@ -127,4 +154,4 @@ def _parse_universe(cls, v: object) -> list[str]: @lru_cache(maxsize=1) def get_evolver_settings() -> EvolverSettings: - return EvolverSettings() # type: ignore[call-arg] \ No newline at end of file + return EvolverSettings() # type: ignore[call-arg] diff --git a/services/evolver/src/inalpha_evolver/data/__init__.py b/services/evolver/src/inalpha_evolver/data/__init__.py new file mode 100644 index 00000000..a0de4a28 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/__init__.py @@ -0,0 +1,6 @@ +"""严格冻结行情数据集。""" + +from .frozen_bars import FrozenBarsLoader +from .manifest import DatasetManifest, FrozenDataset + +__all__ = ["DatasetManifest", "FrozenBarsLoader", "FrozenDataset"] diff --git a/services/evolver/src/inalpha_evolver/data/backfill_audit.py b/services/evolver/src/inalpha_evolver/data/backfill_audit.py new file mode 100644 index 00000000..cdcb21c0 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/backfill_audit.py @@ -0,0 +1,24 @@ +"""Backfill audit snapshot validation.""" +from __future__ import annotations + +from typing import Any + +from inalpha_shared.errors import ValidationError +from pydantic import ValidationError as PydanticValidationError + +from .manifest import BackfillSnapshot + + +def backfill_snapshot(value: dict[str, Any]) -> BackfillSnapshot: + """Preserve the data-service response instead of synthetic counters.""" + try: + return BackfillSnapshot.model_validate(value) + except PydanticValidationError as exc: + raise ValidationError( + "backfill response lacks required audit evidence", + code="EVOLUTION_BACKFILL_AUDIT_INVALID", + details={"errors": exc.errors(include_url=False)}, + ) from exc + + +__all__ = ["backfill_snapshot"] diff --git a/services/evolver/src/inalpha_evolver/data/bar_hash.py b/services/evolver/src/inalpha_evolver/data/bar_hash.py new file mode 100644 index 00000000..960fd25d --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/bar_hash.py @@ -0,0 +1,36 @@ +"""冻结 bars 的稳定内容哈希。""" +from __future__ import annotations + +import hashlib +import struct + +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.market_evaluation import MarketEvaluationContext +from inalpha_paper.model.data import Bar + + +def bars_content_hash( + bars: list[Bar], + instrument: InstrumentId, + context: MarketEvaluationContext, +) -> str: + """按市场身份、时间戳和 OHLCV 的规范二进制编码计算 SHA-256。""" + digest = hashlib.sha256() + identity = f"e1-bars-v1\0{instrument.venue}\0{instrument.symbol}\0{context.canonical_timeframe}\0" + digest.update(identity.encode()) + for bar in bars: + digest.update( + struct.pack( + "!q5d", + bar.ts_event, + bar.open, + bar.high, + bar.low, + bar.close, + bar.volume, + ) + ) + return digest.hexdigest() + + +__all__ = ["bars_content_hash"] diff --git a/services/evolver/src/inalpha_evolver/data/bar_quality.py b/services/evolver/src/inalpha_evolver/data/bar_quality.py new file mode 100644 index 00000000..1949156c --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/bar_quality.py @@ -0,0 +1,92 @@ +"""Frozen bar close, calendar-grid, and content-hash validation.""" +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from inalpha_paper.bar_conversion import bar_from_dict +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.market_cutoff import expected_bar_timestamps, expected_latest_bar_open +from inalpha_paper.market_evaluation import MarketEvaluationContext +from inalpha_paper.model.data import Bar +from inalpha_shared.errors import ValidationError + +from .bar_hash import bars_content_hash +from .bar_validation import validate_identity, validate_values + + +def prepare_frozen_bars( + raw_bars: list[dict[str, Any]], + *, + instrument_id: InstrumentId, + context: MarketEvaluationContext, + as_of: datetime, +) -> tuple[tuple[Bar, ...], str, float, datetime]: + """Remove forming bars and validate a complete connector calendar grid.""" + now = _utc(as_of) + cutoff = expected_latest_bar_open(context, now) + bars: list[Bar] = [] + previous_ts = -1 + for raw in raw_bars: + validate_identity(raw, instrument_id, context.data_timeframe) + bar = bar_from_dict(raw, instrument_id, context.canonical_timeframe) + if _bar_datetime(bar) > cutoff: + continue + if bar.ts_event <= previous_ts: + raise ValidationError( + "bars must be strictly increasing without duplicates", + code="EVOLUTION_DATA_ORDER_INVALID", + ) + validate_values(bar) + bars.append(bar) + previous_ts = bar.ts_event + if len(bars) < 2: + raise ValidationError( + f"evolution needs at least 2 closed bars, got {len(bars)}", + code="EVOLUTION_NO_CLOSED_BARS", + ) + latest = _bar_datetime(bars[-1]) + lag = max(0.0, (cutoff - latest).total_seconds()) + if lag > 0: + raise ValidationError( + f"latest closed bar is {lag:.0f}s behind expected cutoff", + code="EVOLUTION_DATA_FRESHNESS_FAILED", + details={ + "latest_bar_ts": latest.isoformat(), + "cutoff_bar_ts": cutoff.isoformat(), + "lag_seconds": lag, + }, + ) + _validate_grid(bars, context, now) + return tuple(bars), bars_content_hash(bars, instrument_id, context), lag, cutoff + + +def _validate_grid( + bars: list[Bar], + context: MarketEvaluationContext, + as_of: datetime, +) -> None: + expected = expected_bar_timestamps(context, _bar_datetime(bars[0]), as_of) + actual = tuple(_bar_datetime(bar) for bar in bars) + expected_set, actual_set = set(expected), set(actual) + missing = tuple(value for value in expected if value not in actual_set) + unexpected = tuple(value for value in actual if value not in expected_set) + if missing or unexpected: + raise ValidationError( + "bars do not match the connector calendar grid", + code="EVOLUTION_DATA_GAP_INVALID", + details={ + "missing": [value.isoformat() for value in missing[:20]], + "unexpected": [value.isoformat() for value in unexpected[:20]], + }, + ) + + +def _bar_datetime(bar: Bar) -> datetime: + return datetime.fromtimestamp(bar.ts_event / 1_000_000_000, tz=UTC) + + +def _utc(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) diff --git a/services/evolver/src/inalpha_evolver/data/bar_validation.py b/services/evolver/src/inalpha_evolver/data/bar_validation.py new file mode 100644 index 00000000..2bd2d3c9 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/bar_validation.py @@ -0,0 +1,48 @@ +"""Frozen bar identity and OHLCV validation.""" +from __future__ import annotations + +import math +from typing import Any + +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.model.data import Bar +from inalpha_shared.errors import ValidationError + + +def validate_identity( + raw: dict[str, Any], + instrument: InstrumentId, + timeframe: str, +) -> None: + """Fail closed when response identity differs from the request.""" + expected = { + "venue": instrument.venue, + "symbol": instrument.symbol, + "timeframe": timeframe, + } + for field, value in expected.items(): + if field in raw and str(raw[field]) != value: + raise ValidationError( + f"bar {field}={raw[field]!r} does not match request {value!r}", + code="EVOLUTION_DATA_IDENTITY_MISMATCH", + ) + + +def validate_values(bar: Bar) -> None: + """Reject non-finite or internally inconsistent OHLCV values.""" + values = (bar.open, bar.high, bar.low, bar.close, bar.volume) + if not all(math.isfinite(value) for value in values): + raise ValidationError( + "bar contains non-finite value", code="EVOLUTION_DATA_INVALID" + ) + if min(bar.open, bar.high, bar.low, bar.close) <= 0 or bar.volume < 0: + raise ValidationError( + "bar price/volume is invalid", code="EVOLUTION_DATA_INVALID" + ) + if bar.low > min(bar.open, bar.close) or bar.high < max(bar.open, bar.close): + raise ValidationError( + "bar OHLC relation is invalid", code="EVOLUTION_DATA_INVALID" + ) + + +__all__ = ["validate_identity", "validate_values"] diff --git a/services/evolver/src/inalpha_evolver/data/datetime_policy.py b/services/evolver/src/inalpha_evolver/data/datetime_policy.py new file mode 100644 index 00000000..42dde85c --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/datetime_policy.py @@ -0,0 +1,39 @@ +"""Trusted-time policy for frozen evolution datasets.""" +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +from inalpha_shared.errors import ValidationError + +MAX_AS_OF_CLOCK_SKEW = timedelta(seconds=5) + + +def normalize_datetime( + value: datetime, + *, + field: str, + require_aware: bool, +) -> datetime: + """Normalize to UTC, rejecting a naive cutoff instead of raising TypeError.""" + if value.tzinfo is None: + if require_aware: + raise ValidationError( + f"{field} must be timezone-aware", + code="EVOLUTION_DATETIME_NAIVE", + details={"field": field}, + ) + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) + + +def reject_future_as_of(value: datetime) -> None: + """Reject a cutoff beyond trusted wall-clock time.""" + if value > datetime.now(UTC) + MAX_AS_OF_CLOCK_SKEW: + raise ValidationError( + "evolution as_of exceeds trusted current time", + code="EVOLUTION_AS_OF_IN_FUTURE", + details={"as_of": value.isoformat()}, + ) + + +__all__ = ["MAX_AS_OF_CLOCK_SKEW", "normalize_datetime", "reject_future_as_of"] diff --git a/services/evolver/src/inalpha_evolver/data/frozen_bars.py b/services/evolver/src/inalpha_evolver/data/frozen_bars.py new file mode 100644 index 00000000..e6cdc884 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/frozen_bars.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from datetime import UTC, datetime + +from inalpha_paper.data_client import DataClient +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.market_evaluation import build_market_evaluation_context +from inalpha_shared.errors import ValidationError + +from .backfill_audit import backfill_snapshot +from .bar_quality import prepare_frozen_bars +from .datetime_policy import normalize_datetime, reject_future_as_of +from .frozen_io import backfill, read_bars +from .manifest import DatasetManifest, FrozenDataset + + +class FrozenBarsLoader: + def __init__(self, data_client: DataClient) -> None: + self._data_client = data_client + + async def load( + self, + *, + venue: str, + symbol: str, + timeframe: str, + from_ts: datetime, + as_of: datetime, + ) -> FrozenDataset: + start = normalize_datetime(from_ts, field="from_ts", require_aware=False) + cutoff = normalize_datetime(as_of, field="as_of", require_aware=True) + reject_future_as_of(cutoff) + if start >= cutoff: + raise ValidationError( + "evolution from_ts must be earlier than as_of", + code="EVOLUTION_DATA_RANGE_INVALID", + ) + context = build_market_evaluation_context( + venue=venue, symbol=symbol, timeframe=timeframe, as_of=cutoff + ) + backfill_result = await backfill( + self._data_client, venue, symbol, context.data_timeframe, start, cutoff + ) + raw_bars = await read_bars( + self._data_client, venue, symbol, context.data_timeframe, start, cutoff + ) + if len(raw_bars) > 10_000: + raise ValidationError( + "evolution dataset exceeds 10000 bars", + code="EVOLUTION_DATA_LIMIT_EXCEEDED", + ) + bars, content_hash, lag, bar_cutoff = prepare_frozen_bars( + raw_bars, + instrument_id=InstrumentId(symbol=symbol, venue=venue), + context=context, + as_of=cutoff, + ) + first, latest = _bar_time(bars[0].ts_event), _bar_time(bars[-1].ts_event) + manifest = DatasetManifest( + venue=venue, + symbol=symbol, + requested_timeframe=timeframe, + data_timeframe=context.data_timeframe, + canonical_timeframe=context.canonical_timeframe, + requested_from=start, + requested_as_of=cutoff, + effective_from=first, + effective_to=latest, + latest_bar_ts=latest, + cutoff_bar_ts=bar_cutoff, + freshness_lag_seconds=lag, + data_epoch=int(latest.timestamp() * 1000), + bar_count=len(bars), + annualization_periods=context.annualization_periods, + calendar_code=context.calendar_code, + content_sha256=content_hash, + backfill=backfill_snapshot(backfill_result), + ) + return FrozenDataset(bars=bars, manifest=manifest) + + +def _bar_time(value: int) -> datetime: + return datetime.fromtimestamp(value / 1_000_000_000, tz=UTC) + + +__all__ = ["FrozenBarsLoader"] diff --git a/services/evolver/src/inalpha_evolver/data/frozen_io.py b/services/evolver/src/inalpha_evolver/data/frozen_io.py new file mode 100644 index 00000000..99727560 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/frozen_io.py @@ -0,0 +1,60 @@ +"""Fail-closed I/O for frozen bars.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from inalpha_paper.data_client import DataClient, DataServiceError +from inalpha_shared.errors import ValidationError + + +async def backfill( + client: DataClient, + venue: str, + symbol: str, + timeframe: str, + start: datetime, + cutoff: datetime, +) -> dict[str, Any]: + """Backfill once and preserve the upstream response.""" + try: + return await client.backfill_bars( + venue=venue, + symbol=symbol, + timeframe=timeframe, + from_ts=start, + to_ts=cutoff, + ) + except DataServiceError as exc: + code = ( + "EVOLUTION_DATA_UNREACHABLE" + if exc.code == "DATA_SERVICE_UNREACHABLE" + else "EVOLUTION_DATA_FRESHNESS_FAILED" + ) + raise ValidationError(str(exc), code=code) from exc + + +async def read_bars( + client: DataClient, + venue: str, + symbol: str, + timeframe: str, + start: datetime, + cutoff: datetime, +) -> list[dict[str, Any]]: + """Read only the freshly backfilled snapshot without a second refresh.""" + try: + return await client.get_bars( + venue=venue, + symbol=symbol, + timeframe=timeframe, + from_ts=start, + to_ts=cutoff, + limit=10_001, + fresh=False, + ) + except DataServiceError as exc: + raise ValidationError(str(exc), code="EVOLUTION_DATA_UNREACHABLE") from exc + + +__all__ = ["backfill", "read_bars"] diff --git a/services/evolver/src/inalpha_evolver/data/manifest.py b/services/evolver/src/inalpha_evolver/data/manifest.py new file mode 100644 index 00000000..42d42ece --- /dev/null +++ b/services/evolver/src/inalpha_evolver/data/manifest.py @@ -0,0 +1,62 @@ +"""冻结行情的可复核 manifest。""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import TYPE_CHECKING, Literal + +from pydantic import BaseModel, ConfigDict, Field + +if TYPE_CHECKING: + from inalpha_paper.model.data import Bar + + +class BackfillSnapshot(BaseModel): + """data-service 原样返回的回填审计证据。""" + + model_config = ConfigDict(frozen=True) + venue: str + symbol: str + timeframe: str + bars_fetched: int = Field(ge=0) + bars_inserted: int = Field(ge=0) + from_ts: datetime + to_ts: datetime + + +class DatasetManifest(BaseModel): + """一次演化运行唯一使用的数据集身份。""" + + model_config = ConfigDict(frozen=True) + schema_version: Literal["e1.dataset.v2"] = "e1.dataset.v2" + venue: str + symbol: str + requested_timeframe: str + data_timeframe: str + canonical_timeframe: str + requested_from: datetime + requested_as_of: datetime + effective_from: datetime + effective_to: datetime + latest_bar_ts: datetime + cutoff_bar_ts: datetime + freshness_lag_seconds: float = Field(ge=0) + data_epoch: int = Field(ge=0) + bar_count: int = Field(ge=2, le=10_000) + annualization_periods: int = Field(gt=0) + calendar_code: str | None + content_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") + hash_algorithm: Literal["sha256:e1-bars-v1"] = "sha256:e1-bars-v1" + backfill: BackfillSnapshot + warnings: list[str] = Field(default_factory=list, max_length=20) + + +@dataclass(frozen=True, slots=True) +class FrozenDataset: + """不可变 bars 与其 manifest。""" + + bars: tuple[Bar, ...] + manifest: DatasetManifest + + +__all__ = ["BackfillSnapshot", "DatasetManifest", "FrozenDataset"] diff --git a/services/evolver/src/inalpha_evolver/evaluator/__init__.py b/services/evolver/src/inalpha_evolver/evaluator/__init__.py index b6440d68..196c48ec 100644 --- a/services/evolver/src/inalpha_evolver/evaluator/__init__.py +++ b/services/evolver/src/inalpha_evolver/evaluator/__init__.py @@ -1,8 +1,10 @@ from .fitness import compute_fitness_from_report +from .frozen import FrozenDatasetEvaluator from .runner import Evaluator, MockEvaluator __all__ = [ - "compute_fitness_from_report", "Evaluator", + "FrozenDatasetEvaluator", "MockEvaluator", -] \ No newline at end of file + "compute_fitness_from_report", +] diff --git a/services/evolver/src/inalpha_evolver/evaluator/fitness.py b/services/evolver/src/inalpha_evolver/evaluator/fitness.py index 9a91a016..abf7e4e2 100644 --- a/services/evolver/src/inalpha_evolver/evaluator/fitness.py +++ b/services/evolver/src/inalpha_evolver/evaluator/fitness.py @@ -4,14 +4,12 @@ """ from __future__ import annotations -from inalpha_paper.engine.metrics import periods_per_year from inalpha_paper.strategy_authoring.fitness import ( FitnessInputs, compose_fitness, ) from ..exceptions import EvaluationError -from ..population import EvaluationResult def compute_fitness_from_report(report: dict, timeframe: str = "1h") -> float: diff --git a/services/evolver/src/inalpha_evolver/evaluator/frozen.py b/services/evolver/src/inalpha_evolver/evaluator/frozen.py new file mode 100644 index 00000000..8c3544c7 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/evaluator/frozen.py @@ -0,0 +1,74 @@ +"""冻结数据集上的真实策略评估。""" +from __future__ import annotations + +from dataclasses import dataclass + +from inalpha_paper.evaluation_executor import KillableEngineRunner +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.strategy_evaluation import ( + evaluate_buy_and_hold, + evaluate_strategy_source, +) + +from ..data import FrozenDataset +from ..population import EvaluationResult + + +@dataclass(slots=True) +class FrozenDatasetEvaluator: + """所有候选复用同一份 frozen bars 与年化参数。""" + + dataset: FrozenDataset + runner: KillableEngineRunner + initial_cash: float = 10_000.0 + fee_rate: float = 0.001 + validation_split: float = 0.3 + + async def evaluate_baseline(self) -> dict: + """在同一 frozen bars 上计算一次市场买入持有基准。""" + manifest = self.dataset.manifest + instrument = InstrumentId(symbol=manifest.symbol, venue=manifest.venue) + result = await evaluate_buy_and_hold( + bars=list(self.dataset.bars), + instrument_id=instrument, + timeframe=manifest.canonical_timeframe, + run_engine=self.runner, + initial_cash=self.initial_cash, + fee_rate=self.fee_rate, + annualization_periods=float(manifest.annualization_periods), + ) + return result.snapshot.model_dump(mode="json") + + async def evaluate(self, source_code: str) -> EvaluationResult: + manifest = self.dataset.manifest + instrument = InstrumentId(symbol=manifest.symbol, venue=manifest.venue) + result = await evaluate_strategy_source( + source_code=source_code, + bars=list(self.dataset.bars), + instrument_id=instrument, + timeframe=manifest.canonical_timeframe, + run_engine=self.runner, + initial_cash=self.initial_cash, + fee_rate=self.fee_rate, + validation_split=self.validation_split, + annualization_periods=float(manifest.annualization_periods), + ) + return EvaluationResult( + report=result.snapshot.model_dump(mode="json"), + fitness=result.snapshot.fitness, + data_epoch=int(manifest.latest_bar_ts.timestamp() * 1000), + overfitting_risk=_risk(result.snapshot.validation), + ) + + +def _risk(validation: object | None) -> str: + if validation is None: + return "high" + flags = getattr(validation, "flags", []) + decay = getattr(validation, "decay_ratio", None) + if flags or decay is None or decay < 0.5: + return "high" + return "low" if decay >= 0.8 else "medium" + + +__all__ = ["FrozenDatasetEvaluator"] diff --git a/services/evolver/src/inalpha_evolver/evaluator/runner.py b/services/evolver/src/inalpha_evolver/evaluator/runner.py index 957455be..09b08299 100644 --- a/services/evolver/src/inalpha_evolver/evaluator/runner.py +++ b/services/evolver/src/inalpha_evolver/evaluator/runner.py @@ -11,10 +11,8 @@ import asyncio import logging import multiprocessing -import os import resource -import signal -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from functools import partial from typing import Any diff --git a/services/evolver/src/inalpha_evolver/governor/__init__.py b/services/evolver/src/inalpha_evolver/governor/__init__.py index 2cec7195..96b15aea 100644 --- a/services/evolver/src/inalpha_evolver/governor/__init__.py +++ b/services/evolver/src/inalpha_evolver/governor/__init__.py @@ -3,7 +3,7 @@ from .seed import SEED_STRATEGY_CODE __all__ = [ + "SEED_STRATEGY_CODE", "HintGenerator", "run_one_generation", - "SEED_STRATEGY_CODE", ] \ No newline at end of file diff --git a/services/evolver/src/inalpha_evolver/governor/loop.py b/services/evolver/src/inalpha_evolver/governor/loop.py index 52f88ed5..3ca62dcc 100644 --- a/services/evolver/src/inalpha_evolver/governor/loop.py +++ b/services/evolver/src/inalpha_evolver/governor/loop.py @@ -5,14 +5,20 @@ from __future__ import annotations import logging -from datetime import datetime, timezone -from uuid import UUID, uuid4 +from datetime import UTC, datetime +from uuid import UUID from ..evaluator import Evaluator, MockEvaluator -from ..exceptions import DiffApplyError, EvaluationError, EvaluationTimeoutError, LLMError, SandboxError +from ..exceptions import ( + DiffApplyError, + EvaluationError, + EvaluationTimeoutError, + LLMError, + SandboxError, +) from ..mutator import Mutator from ..mutator.mock_client import MockMutator -from ..population import Candidate, EvolutionRun +from ..population import EvolutionRun from ..sandbox import assert_safe, assert_strategy_subclass from .hint_generator import HintGenerator from .seed import SEED_STRATEGY_CODE @@ -58,7 +64,7 @@ async def run_one_generation( seed_strategy_id="sma_cross_v1", budget=budget, config=cfg, - started_at=datetime.now(timezone.utc), + started_at=datetime.now(UTC), ) # 先评估种子策略(baseline) @@ -75,7 +81,7 @@ async def run_one_generation( except (EvaluationError, EvaluationTimeoutError) as exc: logger.error("种子策略评估失败:%s", exc) run.status = "failed" - run.finished_at = datetime.now(timezone.utc) + run.finished_at = datetime.now(UTC) return run seed_report = seed_eval.report @@ -141,31 +147,12 @@ async def run_one_generation( run.failed_eval += 1 continue - # 5. 构建候选记录 - candidate = Candidate( - candidate_id=uuid4(), - run_id=run_id, - generation=1, - parent_id=None, - source_code=mut_res.new_source, - source_hash=mut_res.source_hash, - unified_diff=mut_res.unified_diff, - mutation_hint=hint, - llm_cost_usd=mut_res.llm_cost_usd, - cache_hit_tokens=mut_res.cache_hit_tokens, - fitness=eval_res.fitness, - report=eval_res.report, - overfitting_risk=eval_res.overfitting_risk, - data_epoch=eval_res.data_epoch, - created_at=datetime.now(timezone.utc), - ) - run.candidates_count += 1 logger.info("候选 %d 评估完成:fitness=%.4f", i + 1, eval_res.fitness) # 更新运行状态 run.status = "completed" - run.finished_at = datetime.now(timezone.utc) + run.finished_at = datetime.now(UTC) logger.info( "演化轮次完成:total=%d, ast_rejected=%d, contract_rejected=%d, " diff --git a/services/evolver/src/inalpha_evolver/governor/seed.py b/services/evolver/src/inalpha_evolver/governor/seed.py index 1a991d1d..8e3dba2c 100644 --- a/services/evolver/src/inalpha_evolver/governor/seed.py +++ b/services/evolver/src/inalpha_evolver/governor/seed.py @@ -8,13 +8,6 @@ from typing import Final SEED_STRATEGY_CODE: Final[str] = """ -from collections import deque -from uuid import uuid4 - -from inalpha_paper.model.orders import Order, OrderSide, OrderType, ClientOrderId -from inalpha_paper.model.strategy import Strategy - - class SMACrossStrategy(Strategy): \"\"\"简单双均线交叉策略。 diff --git a/services/evolver/src/inalpha_evolver/governor/seed_resolver.py b/services/evolver/src/inalpha_evolver/governor/seed_resolver.py new file mode 100644 index 00000000..7920baaf --- /dev/null +++ b/services/evolver/src/inalpha_evolver/governor/seed_resolver.py @@ -0,0 +1,56 @@ +"""owner-scoped 种子解析与冻结。""" +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from uuid import UUID + +from inalpha_paper.storage.strategy_candidates import get_owned_candidate +from inalpha_paper.strategy_preparation import prepare_strategy_source +from inalpha_shared.errors import NotFoundError +from psycopg import AsyncConnection + +from .seed import SEED_STRATEGY_CODE + + +@dataclass(frozen=True, slots=True) +class ResolvedSeed: + reference: str + source_code: str + source_hash: str + + +async def resolve_seed( + conn: AsyncConnection, + reference: str, + owner_account_id: UUID, +) -> ResolvedSeed: + if reference == "sma_cross_v1": + source = SEED_STRATEGY_CODE + elif reference.startswith("candidate:"): + try: + candidate_id = UUID(reference.removeprefix("candidate:")) + except ValueError as exc: + raise _not_found(reference) from exc + row = await get_owned_candidate(conn, candidate_id, owner_account_id) + if row is None or row.get("status") != "promoted": + raise _not_found(reference) + source = row["code"] + else: + raise _not_found(reference) + prepare_strategy_source(source) + return ResolvedSeed( + reference=reference, + source_code=source, + source_hash=hashlib.sha256(source.encode()).hexdigest(), + ) + + +def _not_found(reference: str) -> NotFoundError: + return NotFoundError( + f"evolution seed {reference!r} not found", + code="EVOLUTION_SEED_NOT_FOUND", + ) + + +__all__ = ["ResolvedSeed", "resolve_seed"] diff --git a/services/evolver/src/inalpha_evolver/main.py b/services/evolver/src/inalpha_evolver/main.py index a68c96d7..7c660372 100644 --- a/services/evolver/src/inalpha_evolver/main.py +++ b/services/evolver/src/inalpha_evolver/main.py @@ -4,33 +4,48 @@ uvicorn inalpha_evolver.main:app --port 8005 --reload """ + from __future__ import annotations import logging +import os from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from inalpha_shared.db import close_pool, init_pool +from inalpha_shared.middleware import install_error_handler, install_request_logging from .api.routes import router from .config import get_evolver_settings +from .mutator import Mutator +from .runtime import EvolutionRunManager logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): - """服务生命周期管理。 - - E1 暂不连接 DB(使用内存存储)。E2 启 DB pool + LLM client。 - """ + """初始化 DB 队列与 manager;E1 强制单 API worker。""" settings = get_evolver_settings() - logger.info( - "Evolver 服务启动 (model=%s, timeout=%ds)", - settings.llm_model, - settings.evolver_job_timeout_s, + workers = int(os.environ.get("WEB_CONCURRENCY", os.environ.get("WORKERS", "1"))) + if workers != 1: + raise RuntimeError("evolver requires exactly one API worker in E1") + if not settings.database_url: + raise RuntimeError("DATABASE_URL is required for evolver") + await init_pool( + settings.database_url, + min_size=2, + max_size=settings.evolver_pool_size, ) - yield - logger.info("Evolver 服务关闭") + manager = EvolutionRunManager(mutator=Mutator(), settings=settings) + app.state.evolution_manager = manager + await manager.start() + try: + yield + finally: + await manager.close() + await close_pool() app = FastAPI( @@ -41,8 +56,21 @@ async def lifespan(app: FastAPI): ) app.include_router(router) +install_request_logging(app) +install_error_handler(app) -@app.get("/health") -async def health() -> dict[str, str]: - return {"status": "ok", "service": "inalpha-evolver"} \ No newline at end of file +@app.get("/health", response_model=None) +async def health(request: Request) -> dict[str, str] | JSONResponse: + manager = getattr(request.app.state, "evolution_manager", None) + if manager is None or not manager.healthy: + reason = getattr(manager, "unhealthy_reason", None) or "dispatcher unavailable" + return JSONResponse( + status_code=503, + content={ + "status": "unhealthy", + "service": "inalpha-evolver", + "reason": reason, + }, + ) + return {"status": "ok", "service": "inalpha-evolver"} diff --git a/services/evolver/src/inalpha_evolver/mutator/__init__.py b/services/evolver/src/inalpha_evolver/mutator/__init__.py index 90251c5e..0793700c 100644 --- a/services/evolver/src/inalpha_evolver/mutator/__init__.py +++ b/services/evolver/src/inalpha_evolver/mutator/__init__.py @@ -2,9 +2,9 @@ from .llm_client import MutationResult, Mutator __all__ = [ - "apply_diff", - "apply_diff_strict", "DiffApplyError", "MutationResult", "Mutator", + "apply_diff", + "apply_diff_strict", ] \ No newline at end of file diff --git a/services/evolver/src/inalpha_evolver/mutator/diff_applier.py b/services/evolver/src/inalpha_evolver/mutator/diff_applier.py index 3fb0272d..15c3fc22 100644 --- a/services/evolver/src/inalpha_evolver/mutator/diff_applier.py +++ b/services/evolver/src/inalpha_evolver/mutator/diff_applier.py @@ -1,174 +1,118 @@ -"""unified diff 应用器 —— 用标准库 difflib 替换 unidiff,宽容 LLM 的输出格式。 +"""严格、原子地应用单文件 unified diff。""" -unidiff 0.7.5 对 diff 格式要求极其严格,而 LLM(尤其 DeepSeek)生成的 diff -常带 markdown fence、省略空行、行号偏移 1-3 行。直接用 Python 内置 difflib 解析 -和应用,比第三方库更宽容。 -""" from __future__ import annotations import re -from typing import Final +from dataclasses import dataclass from ..exceptions import DiffApplyError -_DEFAULT_FUZZ: Final[int] = 3 +_HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$") +_OLD_PATH = "--- a/strategy.py" +_NEW_PATH = "+++ b/strategy.py" -def _clean_diff(raw_diff: str) -> str: - """清洗 LLM 输出:剥 markdown fence + 定位 diff 块。""" - text = raw_diff.strip() - lines = text.split("\n") - # 剥外层 ``` - if lines and lines[0].startswith("```"): - lines = lines[1:] - if lines and lines[-1].strip() in ("```", "```diff"): - lines = lines[:-1] - text = "\n".join(lines).strip() - # 在内容中找 diff 块 - marker = "--- a/" - idx = text.find(marker) - if idx >= 0: - text = text[idx:] - # 截断在 ``` 之前 - end = text.find("\n```") - if end >= 0: - text = text[:end] - return text.strip() - - -def _lines(text: str) -> list[str]: - return text.splitlines(keepends=True) - - -_HUNK_RE = re.compile(r"^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@ ?(.*)$") - - -def _parse_hunk_header(line: str) -> tuple[int, int] | None: - """解析 @@ -old,count +new,count @@ → (old_start, old_count)。""" - m = _HUNK_RE.match(line) - if not m: - return None - start = int(m.group(1)) - count = int(m.group(2)) if m.group(2) else 1 - return start, count - - -def apply_diff( - original: str, - unified_diff: str, - max_fuzz: int = _DEFAULT_FUZZ, -) -> str: - if not unified_diff.strip(): - return original +@dataclass(frozen=True, slots=True) +class _Hunk: + old_start: int + old_count: int + new_start: int + new_count: int + body: tuple[tuple[str, str], ...] - diff_text = _clean_diff(unified_diff) - lines = _lines(original) - diff_lines = diff_text.split("\n") +def _fail(message: str, original: str, diff: str) -> DiffApplyError: + return DiffApplyError(message, original=original, failed_diff=diff) - # 跳过 header (--- / +++ 行) - i = 0 - while i < len(diff_lines) and ( - diff_lines[i].startswith("---") or diff_lines[i].startswith("+++") - ): - i += 1 - if not any(l.startswith("@@") for l in diff_lines): - raise DiffApplyError( - "unified diff 不包含任何 hunk", - original=original, failed_diff=unified_diff, +def _clean(raw_diff: str, original: str) -> list[str]: + text = raw_diff.strip() + if text.startswith("```"): + lines = text.splitlines() + if lines[0] not in {"```", "```diff"} or lines[-1] != "```": + raise _fail("diff markdown fence 格式无效", original, raw_diff) + text = "\n".join(lines[1:-1]).strip() + lines = text.splitlines() + if len(lines) < 3 or lines[:2] != [_OLD_PATH, _NEW_PATH]: + raise _fail( + "diff 必须且只能修改 a/strategy.py -> b/strategy.py", + original, + raw_diff, ) + if any(line.startswith(("--- ", "+++ ")) for line in lines[2:]): + raise _fail("diff 不允许包含第二个文件", original, raw_diff) + return lines[2:] - while i < len(diff_lines): - line = diff_lines[i] - if not line.startswith("@@"): - i += 1 - continue - parsed = _parse_hunk_header(line) - if parsed is None: - i += 1 +def _parse_hunks(lines: list[str], original: str, raw_diff: str) -> list[_Hunk]: + hunks: list[_Hunk] = [] + index = 0 + while index < len(lines): + if lines[index] == "": + index += 1 continue - - old_start, _old_count = parsed - i += 1 - - # 收集 hunk body - hunk_body: list[tuple[str, str]] = [] - while i < len(diff_lines): - l = diff_lines[i] - if l.startswith("@@"): + match = _HUNK_RE.fullmatch(lines[index]) + if match is None: + raise _fail(f"无效 hunk header: {lines[index]!r}", original, raw_diff) + old_start, old_raw, new_start, new_raw = match.groups() + old_count = int(old_raw) if old_raw is not None else 1 + new_count = int(new_raw) if new_raw is not None else 1 + index += 1 + body: list[tuple[str, str]] = [] + seen_old = seen_new = 0 + while index < len(lines) and not lines[index].startswith("@@"): + line = lines[index] + if line == "" and seen_old == old_count and seen_new == new_count: + index += 1 break - if l.startswith(" "): - hunk_body.append(("context", l[1:])) - elif l.startswith("-"): - hunk_body.append(("removed", l[1:])) - elif l.startswith("+"): - hunk_body.append(("added", l[1:])) - i += 1 - - if not hunk_body: - continue - - # 在源码中定位匹配位置 - pos = _find_hunk_position(lines, hunk_body, old_start, max_fuzz) - - if pos is None: - raise DiffApplyError( - f"hunk 行号 {old_start} 无法匹配,尝试 fuzz={max_fuzz}", - original=original, - failed_diff=unified_diff, - ) - - # 应用 hunk:src = context + removed,tgt = context + added - src_lines = [t for kind, t in hunk_body if kind in ("context", "removed")] - tgt_lines = [t for kind, t in hunk_body if kind in ("context", "added")] - - # 转换回带 \n 的格式以匹配源文件 - src_with_nl = [l + "\n" if not l.endswith("\n") else l for l in src_lines] - tgt_with_nl = [l + "\n" if not l.endswith("\n") else l for l in tgt_lines] - - lines = lines[:pos] + tgt_with_nl + lines[pos + len(src_with_nl):] - - return "".join(lines) - - -def _find_hunk_position( - lines: list[str], - hunk_body: list[tuple[str, str]], - old_start: int, - max_fuzz: int, -) -> int | None: - """在源码中定位 hunk 的匹配位置。 - - 用完整的 source side 行(context + removed)做连续匹配。 - """ - # source side = context + removed(hunk 在原文件中的连续行) - src_lines = [t for kind, t in hunk_body if kind in ("context", "removed")] - if not src_lines: - return None - - lines_stripped = [l.rstrip("\n") for l in lines] - src_stripped = [l.rstrip("\n") if l.endswith("\n") else l for l in src_lines] - - # 1. header 行号 ± fuzz - start = max(0, old_start - 1) - search_start = max(0, start - max_fuzz) - search_end = min(len(lines_stripped), start + max_fuzz + len(src_stripped)) - - for offset in range(search_start, search_end - len(src_stripped) + 1): - if lines_stripped[offset : offset + len(src_stripped)] == src_stripped: - return offset - - # 2. 全文件扫描(max_fuzz > 0 时) - if max_fuzz > 0: - for offset in range(len(lines_stripped) - len(src_stripped) + 1): - if lines_stripped[offset : offset + len(src_stripped)] == src_stripped: - return offset - - return None - + if not line or line[0] not in {" ", "+", "-"}: + raise _fail(f"无效 hunk 行: {line!r}", original, raw_diff) + prefix, content = line[0], line[1:] + body.append((prefix, content)) + seen_old += prefix in {" ", "-"} + seen_new += prefix in {" ", "+"} + if seen_old > old_count or seen_new > new_count: + raise _fail("hunk 行数超过 header 声明", original, raw_diff) + index += 1 + if seen_old != old_count or seen_new != new_count: + raise _fail("hunk 行数与 header 声明不一致", original, raw_diff) + if not any(prefix in {"+", "-"} for prefix, _ in body): + raise _fail("hunk 未包含任何变更", original, raw_diff) + hunks.append(_Hunk(int(old_start), old_count, int(new_start), new_count, tuple(body))) + if not hunks: + raise _fail("unified diff 不包含任何 hunk", original, raw_diff) + return hunks + + +def apply_diff(original: str, unified_diff: str, max_fuzz: int = 0) -> str: + """按 header 的精确位置原子应用 diff;``max_fuzz`` 仅为兼容旧调用。""" + del max_fuzz + if not unified_diff.strip(): + return original + hunks = _parse_hunks(_clean(unified_diff, original), original, unified_diff) + source = original.splitlines() + output: list[str] = [] + cursor = 0 + delta = 0 + for hunk in hunks: + old_index = hunk.old_start - 1 if hunk.old_count else hunk.old_start + new_index = hunk.new_start - 1 if hunk.new_count else hunk.new_start + if old_index < cursor or old_index > len(source): + raise _fail("hunk 重叠或 old_start 越界", original, unified_diff) + if new_index != old_index + delta: + raise _fail("hunk new_start 与前序变更不一致", original, unified_diff) + expected = [text for prefix, text in hunk.body if prefix in {" ", "-"}] + if source[old_index : old_index + len(expected)] != expected: + raise _fail("hunk 上下文与声明位置不完全匹配", original, unified_diff) + output.extend(source[cursor:old_index]) + output.extend(text for prefix, text in hunk.body if prefix in {" ", "+"}) + cursor = old_index + len(expected) + delta += hunk.new_count - hunk.old_count + output.extend(source[cursor:]) + result = "\n".join(output) + return result + "\n" if original.endswith("\n") else result def apply_diff_strict(original: str, unified_diff: str) -> str: - return apply_diff(original, unified_diff, max_fuzz=0) \ No newline at end of file + """兼容旧 API;所有 diff 现在都使用严格模式。""" + return apply_diff(original, unified_diff) diff --git a/services/evolver/src/inalpha_evolver/mutator/mock_client.py b/services/evolver/src/inalpha_evolver/mutator/mock_client.py index 16b13b1b..8595ff37 100644 --- a/services/evolver/src/inalpha_evolver/mutator/mock_client.py +++ b/services/evolver/src/inalpha_evolver/mutator/mock_client.py @@ -5,8 +5,6 @@ from hashlib import sha256 from typing import Any -from inalpha_shared_llm.client import MockLLMClient - from .diff_applier import apply_diff from .llm_client import MutationResult diff --git a/services/evolver/src/inalpha_evolver/runtime/__init__.py b/services/evolver/src/inalpha_evolver/runtime/__init__.py new file mode 100644 index 00000000..42505eb2 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/__init__.py @@ -0,0 +1,5 @@ +"""Evolver 运行时。""" + +from .manager import EvolutionRunManager + +__all__ = ["EvolutionRunManager"] diff --git a/services/evolver/src/inalpha_evolver/runtime/dispatcher.py b/services/evolver/src/inalpha_evolver/runtime/dispatcher.py new file mode 100644 index 00000000..05fc9fb5 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/dispatcher.py @@ -0,0 +1,53 @@ +"""E1 数据库队列 dispatcher。""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from inalpha_shared.db import get_conn + +from ..storage import run_queries + + +async def dispatch_runs(manager: Any) -> None: + """持续 claim queued run;暂时性 DB 异常不会杀死 dispatcher。""" + delay = 0.1 + while not manager.closing: + acquired = handed_off = False + try: + await manager.semaphore.acquire() + acquired = True + async with get_conn() as conn: + run = await run_queries.claim_next(conn) + manager.unhealthy_reason = None + delay = 0.1 + if run is None: + manager.semaphore.release() + acquired = False + await _wait_for_work(manager.wake) + continue + task = asyncio.create_task(manager._execute(run), name=f"evo-{run['run_id']}") + manager.tasks[run["run_id"]] = task + task.add_done_callback(lambda done, rid=run["run_id"]: manager._done(rid, done)) + handed_off = True + except asyncio.CancelledError: + raise + except Exception as exc: + manager.unhealthy_reason = f"dispatcher failed: {type(exc).__name__}: {exc}" + await asyncio.sleep(delay) + delay = min(delay * 2, 5.0) + finally: + if acquired and not handed_off: + manager.semaphore.release() + + +async def _wait_for_work(wake: asyncio.Event) -> None: + wake.clear() + try: + await asyncio.wait_for(wake.wait(), timeout=1.0) + except TimeoutError: + pass + + +__all__ = ["dispatch_runs"] diff --git a/services/evolver/src/inalpha_evolver/runtime/executor.py b/services/evolver/src/inalpha_evolver/runtime/executor.py new file mode 100644 index 00000000..652cd853 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/executor.py @@ -0,0 +1,82 @@ +"""queued run 的数据加载、评估器构建与执行。""" +from __future__ import annotations + +import asyncio +import time +from datetime import datetime +from typing import Any +from uuid import UUID + +import jwt +from inalpha_paper.data_client import DataClient +from inalpha_paper.evaluation_executor import KillableEngineRunner +from inalpha_shared.db import get_conn + +from ..config import EvolverSettings +from ..data import FrozenBarsLoader +from ..evaluator import FrozenDatasetEvaluator +from ..storage import runs +from .generation import execute_generation + + +async def execute_run( + run: dict[str, Any], + *, + mutator: Any, + settings: EvolverSettings, +) -> None: + config = _parse_config(run["config"]) + run = {**run, "config": config} + token = _service_token(run["owner_account_id"], settings) + async with DataClient(settings.data_service_url, token) as client: + dataset = await FrozenBarsLoader(client).load( + venue=config["venue"], + symbol=config["symbol"], + timeframe=config["timeframe"], + from_ts=config["from_ts"], + as_of=config["as_of"], + ) + async with get_conn() as conn: + current = await runs.transition( + conn, + run["run_id"], + from_statuses=("running",), + to_status="running", + values={ + "data_timeframe": dataset.manifest.data_timeframe, + "engine_timeframe": dataset.manifest.canonical_timeframe, + "dataset_manifest": dataset.manifest.model_dump(mode="json"), + "active_stage": "evaluating_seed", + }, + ) + if current is None: + raise asyncio.CancelledError + evaluator = FrozenDatasetEvaluator( + dataset=dataset, + runner=KillableEngineRunner( + timeout_s=settings.evolver_job_timeout_s, + mem_gb=settings.evolver_job_mem_gb, + ), + initial_cash=float(config["initial_cash"]), + fee_rate=float(config.get("fee_rate", 0.001)), + validation_split=float(config.get("validation_split", 0.3)), + ) + await execute_generation(run, mutator=mutator, evaluator=evaluator) + + +def _parse_config(config: dict[str, Any]) -> dict[str, Any]: + """把 JSONB 中的 ISO 时间恢复为 loader 所需的 datetime。""" + parsed = dict(config) + for key in ("from_ts", "as_of"): + value = parsed.get(key) + if isinstance(value, str): + parsed[key] = datetime.fromisoformat(value.replace("Z", "+00:00")) + return parsed + + +def _service_token(account_id: UUID, settings: EvolverSettings) -> str: + return jwt.encode( + {"sub": str(account_id), "exp": int(time.time()) + settings.service_token_ttl_s}, + settings.jwt_secret, + algorithm=settings.jwt_algorithm, + ) diff --git a/services/evolver/src/inalpha_evolver/runtime/finalizer.py b/services/evolver/src/inalpha_evolver/runtime/finalizer.py new file mode 100644 index 00000000..07c8432f --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/finalizer.py @@ -0,0 +1,119 @@ +"""run 执行期限、终态与遗留 slot 的可重试收口。""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import UTC, datetime +from typing import Any +from uuid import UUID + +from inalpha_shared.db import get_conn + +from ..storage import candidates, runs +from .executor import execute_run + + +async def execute_managed( + run: dict[str, Any], + *, + mutator: Any, + settings: Any, + should_stop: Callable[[], bool], + on_error: Callable[[str], None], + on_success: Callable[[], None], +) -> None: + """执行一个 run,并保证取消、失败和总超时都进入终态。""" + timeout = asyncio.timeout(float(getattr(settings, "evolver_run_timeout_s", 1200))) + try: + async with timeout: + await execute_run(run, mutator=mutator, settings=settings) + except asyncio.CancelledError: + await _finalize( + run["run_id"], + aborted=True, + error=None, + should_stop=should_stop, + on_error=on_error, + on_success=on_success, + ) + raise + except TimeoutError as exc: + error = ( + _RunTimeoutError("evolution run exceeded its total deadline") + if timeout.expired() + else exc + ) + await _finalize( + run["run_id"], + aborted=False, + error=error, + should_stop=should_stop, + on_error=on_error, + on_success=on_success, + ) + except Exception as exc: + await _finalize( + run["run_id"], + aborted=False, + error=exc, + should_stop=should_stop, + on_error=on_error, + on_success=on_success, + ) + + +async def _finalize( + run_id: UUID, + *, + aborted: bool, + error: BaseException | None, + should_stop: Callable[[], bool], + on_error: Callable[[str], None], + on_success: Callable[[], None], +) -> None: + status = "aborted" if aborted else "failed" + code = ( + "EVOLUTION_ABORTED" if aborted else str(getattr(error, "code", "EVOLUTION_INTERNAL_ERROR")) + ) + message = "run cancelled" if aborted else str(error)[:1000] + values = { + "active_stage": status, + "finished_at": datetime.now(UTC), + "failure_code": code, + "failure_message": message, + } + delay = 0.1 + while True: + try: + async with get_conn() as conn: + await candidates.close_pending( + conn, + run_id, + error_code=code, + error_message=message, + ) + await runs.transition( + conn, + run_id, + from_statuses=("running", "cancelling") if aborted else ("running",), + to_status=status, + values=values, + ) + on_success() + return + except asyncio.CancelledError: + raise + except Exception as exc: + on_error(f"finalize {run_id} failed: {type(exc).__name__}: {exc}") + if should_stop(): + return + await asyncio.sleep(delay) + delay = min(delay * 2, 5.0) + + +class _RunTimeoutError(TimeoutError): + code = "EVOLUTION_RUN_TIMEOUT" + + +__all__ = ["execute_managed"] diff --git a/services/evolver/src/inalpha_evolver/runtime/generation.py b/services/evolver/src/inalpha_evolver/runtime/generation.py new file mode 100644 index 00000000..182d7413 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/generation.py @@ -0,0 +1,99 @@ +"""单代演化 run 的顺序调度。""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime +from typing import Any +from uuid import UUID + +from inalpha_shared.db import get_conn + +from ..evaluator.frozen import FrozenDatasetEvaluator +from ..exceptions import DiffApplyError, LLMError +from ..governor.hint_generator import HintGenerator +from ..storage import candidates, runs +from .slots import evaluate_slot, persist_mutation, reject_slot + + +async def execute_generation( + run: dict[str, Any], + *, + mutator: Any, + evaluator: FrozenDatasetEvaluator, +) -> None: + source = run["seed_source_snapshot"] + seed_result, baseline_snapshot = await _evaluate_seed_and_baseline(evaluator, source) + async with get_conn() as conn: + current = await runs.transition( + conn, + run["run_id"], + from_statuses=("running",), + to_status="running", + values={ + "seed_report_snapshot": seed_result.report, + "baseline_snapshot": baseline_snapshot, + "active_stage": "mutating", + }, + ) + if current is None: + raise asyncio.CancelledError + hints = HintGenerator() + for slot in range(run["budget"]): + await _checkpoint(run["run_id"]) + hint = hints.next() + async with get_conn() as conn: + await candidates.insert_slot(conn, run["run_id"], slot, hint) + try: + mutation = await mutator.mutate(source, seed_result.report, hint) + except LLMError as exc: + await reject_slot(run["run_id"], slot, "mutation_failed", exc) + continue + except DiffApplyError as exc: + await reject_slot(run["run_id"], slot, "diff_failed", exc) + continue + candidate_source = await persist_mutation(run["run_id"], slot, mutation) + if candidate_source is not None: + await evaluate_slot(run["run_id"], slot, candidate_source, evaluator) + async with get_conn() as conn: + summary = await candidates.summarize(conn, run["run_id"]) + completed = await runs.transition( + conn, + run["run_id"], + from_statuses=("running",), + to_status="completed", + values={ + "active_stage": "completed", + "finished_at": datetime.now(UTC), + "llm_cost_usd": summary["llm_cost_usd"], + }, + ) + if completed is None: + raise asyncio.CancelledError + + +async def _evaluate_seed_and_baseline( + evaluator: FrozenDatasetEvaluator, + source: str, +) -> tuple[Any, dict[str, Any]]: + """任一评估失败时取消并等待 sibling,避免越过 run 并发预算。""" + tasks = [ + asyncio.create_task(evaluator.evaluate(source)), + asyncio.create_task(evaluator.evaluate_baseline()), + ] + try: + seed_result, baseline = await asyncio.gather(*tasks) + return seed_result, baseline + except BaseException: + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + + +async def _checkpoint(run_id: UUID) -> None: + async with get_conn() as conn: + current = await runs.get_run(conn, run_id) + if current is None or current["status"] != "running": + raise asyncio.CancelledError diff --git a/services/evolver/src/inalpha_evolver/runtime/manager.py b/services/evolver/src/inalpha_evolver/runtime/manager.py new file mode 100644 index 00000000..445a0225 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/manager.py @@ -0,0 +1,81 @@ +"""数据库队列驱动的 E1 run manager。""" + +from __future__ import annotations + +import asyncio +from typing import Any +from uuid import UUID + +from inalpha_shared.db import get_conn + +from ..config import EvolverSettings +from ..storage import run_queries +from .dispatcher import dispatch_runs +from .finalizer import execute_managed + + +class EvolutionRunManager: + def __init__(self, *, mutator: Any, settings: EvolverSettings) -> None: + self.mutator = mutator + self.settings = settings + self.tasks: dict[UUID, asyncio.Task[None]] = {} + self.wake = asyncio.Event() + self.dispatcher: asyncio.Task[None] | None = None + self.semaphore = asyncio.Semaphore(settings.evolver_max_running_runs) + self.closing = False + self.unhealthy_reason: str | None = None + + @property + def healthy(self) -> bool: + """dispatcher 存活且没有尚未恢复的队列或终态写入错误。""" + return bool( + self.dispatcher and not self.dispatcher.done() and self.unhealthy_reason is None + ) + + async def start(self) -> None: + async with get_conn() as conn: + await run_queries.reconcile_interrupted(conn) + self.dispatcher = asyncio.create_task(dispatch_runs(self), name="evo-dispatch") + + async def notify_async(self) -> None: + self.wake.set() + + async def abort(self, run_id: UUID) -> None: + task = self.tasks.get(run_id) + if task is not None and task.cancelling() == 0: + task.cancel() + + async def close(self) -> None: + self.closing = True + self.wake.set() + if self.dispatcher: + self.dispatcher.cancel() + tasks = list(self.tasks.values()) + for task in tasks: + if task.cancelling() == 0: + task.cancel() + pending = tasks + ([self.dispatcher] if self.dispatcher else []) + await asyncio.gather(*pending, return_exceptions=True) + close = getattr(getattr(self.mutator, "llm_client", None), "close", None) + if close is not None: + await close() + + async def _execute(self, run: dict[str, Any]) -> None: + await execute_managed( + run, + mutator=self.mutator, + settings=self.settings, + should_stop=lambda: self.closing, + on_error=lambda reason: setattr(self, "unhealthy_reason", reason), + on_success=lambda: setattr(self, "unhealthy_reason", None), + ) + + def _done(self, run_id: UUID, task: asyncio.Task[None]) -> None: + self.tasks.pop(run_id, None) + self.semaphore.release() + self.wake.set() + if not task.cancelled(): + task.exception() + + +__all__ = ["EvolutionRunManager"] diff --git a/services/evolver/src/inalpha_evolver/runtime/slots.py b/services/evolver/src/inalpha_evolver/runtime/slots.py new file mode 100644 index 00000000..ee146571 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/runtime/slots.py @@ -0,0 +1,110 @@ +"""演化 slot 的准备、评估与持久化。""" + +from __future__ import annotations + +from typing import Any +from uuid import UUID + +from inalpha_paper.evaluation_executor import WorkerExecutionError +from inalpha_paper.strategy_preparation import audit_strategy_source +from inalpha_shared.db import get_conn +from inalpha_shared.errors import ValidationError + +from ..evaluator.frozen import FrozenDatasetEvaluator +from ..storage import candidates + + +async def persist_mutation( + run_id: UUID, + slot: int, + mutation: Any, +) -> str | None: + """校验变异;失败落终态,成功落源码并返回。""" + if mutation.unified_diff is None: + await reject_slot(run_id, slot, "no_change", None) + return None + try: + audited_source = audit_strategy_source(mutation.new_source) + except ValidationError as exc: + await reject_slot(run_id, slot, "ast_rejected", exc) + return None + async with get_conn() as conn: + if await candidates.source_exists(conn, run_id, mutation.source_hash): + await candidates.update_slot( + conn, + run_id, + slot, + stage="completed", + outcome="duplicate", + unified_diff=mutation.unified_diff, + llm_cost_usd=mutation.llm_cost_usd, + cache_hit_tokens=mutation.cache_hit_tokens, + ) + return None + await candidates.update_slot( + conn, + run_id, + slot, + stage="evaluation", + source_code=audited_source, + source_hash=mutation.source_hash, + unified_diff=mutation.unified_diff, + llm_cost_usd=mutation.llm_cost_usd, + cache_hit_tokens=mutation.cache_hit_tokens, + audit_snapshot={"ok": True, "mode": "static_ast"}, + contract_snapshot={"ok": False, "status": "pending_worker"}, + ) + return audited_source + + +async def evaluate_slot( + run_id: UUID, + slot: int, + source: str, + evaluator: FrozenDatasetEvaluator, +) -> None: + try: + result = await evaluator.evaluate(source) + except WorkerExecutionError as exc: + outcome = { + "CANDIDATE_REAUDIT_FAILED": "ast_rejected", + "CANDIDATE_LOAD_FAILED": "contract_rejected", + "CANDIDATE_CONTRACT_FAILED": "contract_rejected", + }.get(exc.code, "evaluation_failed") + await reject_slot(run_id, slot, outcome, exc) + return + except TimeoutError as exc: + await reject_slot(run_id, slot, "evaluation_failed", exc) + return + async with get_conn() as conn: + await candidates.update_slot( + conn, + run_id, + slot, + stage="completed", + outcome="succeeded", + fitness=result.fitness, + evaluation_snapshot=result.report, + contract_snapshot={"ok": True, "validated_in": "worker"}, + report=result.report, + data_epoch=result.data_epoch, + overfitting_risk=result.overfitting_risk, + ) + + +async def reject_slot( + run_id: UUID, + slot: int, + outcome: str, + error: BaseException | None, +) -> None: + async with get_conn() as conn: + await candidates.update_slot( + conn, + run_id, + slot, + stage="completed", + outcome=outcome, + error_code=getattr(error, "code", None), + error_message=str(error)[:1000] if error else None, + ) diff --git a/services/evolver/src/inalpha_evolver/storage/__init__.py b/services/evolver/src/inalpha_evolver/storage/__init__.py new file mode 100644 index 00000000..a6113594 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/storage/__init__.py @@ -0,0 +1,5 @@ +"""Evolver 持久化存储。""" + +from . import candidates, runs + +__all__ = ["candidates", "runs"] diff --git a/services/evolver/src/inalpha_evolver/storage/candidates.py b/services/evolver/src/inalpha_evolver/storage/candidates.py new file mode 100644 index 00000000..05325737 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/storage/candidates.py @@ -0,0 +1,127 @@ +"""strategy_evo_candidates 的 slot 持久化。""" + +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import Any +from uuid import UUID, uuid4 + +from psycopg import AsyncConnection + +_COLUMNS = """candidate_id,run_id,slot,generation,parent_id,stage,outcome,source_code, +source_hash,unified_diff,mutation_hint,llm_cost_usd,cache_hit_tokens,input_tokens, +output_tokens,fitness,evaluation_snapshot,audit_snapshot,contract_snapshot,error_code, +error_message,overfitting_risk,data_epoch,created_at,updated_at""" + + +async def insert_slot( + conn: AsyncConnection, + run_id: UUID, + slot: int, + mutation_hint: str, +) -> dict[str, Any]: + async with conn.cursor() as cur: + await cur.execute( + f"""INSERT INTO strategy_evo_candidates(candidate_id,run_id,slot,generation, +mutation_hint,stage,outcome,status,report)VALUES(%s,%s,%s,1,%s,'mutation','pending', +'evaluated',NULL)ON CONFLICT(run_id,slot)DO UPDATE SET updated_at=NOW() +RETURNING {_COLUMNS}""", + (uuid4(), run_id, slot, mutation_hint), + ) + row = await cur.fetchone() + return dict(row) + + +async def update_slot( + conn: AsyncConnection, + run_id: UUID, + slot: int, + **values: Any, +) -> dict[str, Any] | None: + values["updated_at"] = datetime.now(UTC) + assignments = ",".join(f"{key}=%s" for key in values) + params = [json.dumps(value) if isinstance(value, dict) else value for value in values.values()] + params.extend([run_id, slot]) + async with conn.cursor() as cur: + await cur.execute( + f"UPDATE strategy_evo_candidates SET {assignments} WHERE run_id=%s AND slot=%s RETURNING {_COLUMNS}", + params, + ) + row = await cur.fetchone() + return dict(row) if row else None + + +async def source_exists( + conn: AsyncConnection, + run_id: UUID, + source_hash: str, +) -> bool: + """判断 run 内是否已有相同非空候选源码。""" + async with conn.cursor() as cur: + await cur.execute( + "SELECT 1 FROM strategy_evo_candidates WHERE run_id=%s AND source_hash=%s LIMIT 1", + (run_id, source_hash), + ) + return await cur.fetchone() is not None + + +async def list_candidates( + conn: AsyncConnection, + run_id: UUID, + owner_account_id: UUID, +) -> list[dict[str, Any]]: + selected = ",".join(f"c.{name.strip()}" for name in _COLUMNS.split(",")) + async with conn.cursor() as cur: + await cur.execute( + f"""SELECT {selected} FROM strategy_evo_candidates c JOIN strategy_evo_runs r +USING(run_id)WHERE c.run_id=%s AND r.owner_account_id=%s ORDER BY c.slot""", + (run_id, owner_account_id), + ) + rows = await cur.fetchall() + return [dict(row) for row in rows] + + +async def get_candidate( + conn: AsyncConnection, + candidate_id: UUID, + owner_account_id: UUID, +) -> dict[str, Any] | None: + selected = ",".join(f"c.{name.strip()}" for name in _COLUMNS.split(",")) + async with conn.cursor() as cur: + await cur.execute( + f"""SELECT {selected} FROM strategy_evo_candidates c JOIN strategy_evo_runs r +USING(run_id)WHERE c.candidate_id=%s AND r.owner_account_id=%s""", + (candidate_id, owner_account_id), + ) + row = await cur.fetchone() + return dict(row) if row else None + + +async def close_pending( + conn: AsyncConnection, + run_id: UUID, + *, + error_code: str, + error_message: str, +) -> int: + """把 run 终态遗留的 pending slot 原子收口为 cancelled。""" + async with conn.cursor() as cur: + await cur.execute( + """UPDATE strategy_evo_candidates SET stage='completed',outcome='cancelled', +error_code=%s,error_message=%s,updated_at=%s WHERE run_id=%s AND outcome='pending'""", + (error_code, error_message[:1000], datetime.now(UTC), run_id), + ) + return cur.rowcount + + +async def summarize(conn: AsyncConnection, run_id: UUID) -> dict[str, int | float]: + async with conn.cursor() as cur: + await cur.execute( + """SELECT count(*) attempted,count(*)FILTER(WHERE outcome='succeeded') succeeded, +count(*)FILTER(WHERE outcome NOT IN('pending','succeeded')) rejected, +COALESCE(sum(llm_cost_usd),0) llm_cost_usd FROM strategy_evo_candidates WHERE run_id=%s""", + (run_id,), + ) + row = await cur.fetchone() + return dict(row) diff --git a/services/evolver/src/inalpha_evolver/storage/run_queries.py b/services/evolver/src/inalpha_evolver/storage/run_queries.py new file mode 100644 index 00000000..34a17144 --- /dev/null +++ b/services/evolver/src/inalpha_evolver/storage/run_queries.py @@ -0,0 +1,103 @@ +"""run 列表、claim 与重启收口 SQL。""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any +from uuid import UUID + +from psycopg import AsyncConnection + +from .runs import _COLUMNS + +_RUN_COLUMNS = ",".join(f"r.{name.strip()}" for name in _COLUMNS.split(",")) +_SUMMARY_COLUMNS = """COALESCE(s.attempted,0) attempted,COALESCE(s.succeeded,0) succeeded, +COALESCE(s.rejected,0) rejected""" + + +async def list_runs( + conn: AsyncConnection, + owner_account_id: UUID, + *, + limit: int, + cursor: tuple[datetime, UUID] | None = None, +) -> list[dict[str, Any]]: + sql = f"""SELECT {_RUN_COLUMNS},{_SUMMARY_COLUMNS} FROM strategy_evo_runs r +LEFT JOIN LATERAL(SELECT count(*) attempted, +count(*)FILTER(WHERE outcome='succeeded') succeeded, +count(*)FILTER(WHERE outcome NOT IN('pending','succeeded')) rejected +FROM strategy_evo_candidates WHERE run_id=r.run_id)s ON TRUE +WHERE r.owner_account_id=%s""" + params: list[Any] = [owner_account_id] + if cursor is not None: + sql += " AND (r.queued_at,r.run_id)<(%s,%s)" + params.extend(cursor) + sql += " ORDER BY r.queued_at DESC,r.run_id DESC LIMIT %s" + params.append(limit) + async with conn.cursor() as cur: + await cur.execute(sql, params) + rows = await cur.fetchall() + return [dict(row) for row in rows] + + +async def claim_next(conn: AsyncConnection) -> dict[str, Any] | None: + """锁定最早 queued run 并原子切换为 running。""" + now = datetime.now(UTC) + async with conn.cursor() as cur: + await cur.execute( + f"""WITH picked AS(SELECT run_id FROM strategy_evo_runs WHERE status='queued' +ORDER BY queued_at FOR UPDATE SKIP LOCKED LIMIT 1)UPDATE strategy_evo_runs r SET +status='running',active_stage='loading_data',started_at=%s,updated_at=%s FROM picked +WHERE r.run_id=picked.run_id RETURNING {",".join(f"r.{name.strip()}" for name in _COLUMNS.split(","))}""", + (now, now), + ) + row = await cur.fetchone() + return dict(row) if row else None + + +async def count_active(conn: AsyncConnection, owner_account_id: UUID) -> int: + async with conn.cursor() as cur: + await cur.execute( + "SELECT count(*) n FROM strategy_evo_runs WHERE owner_account_id=%s AND status=ANY(%s)", + (owner_account_id, ["queued", "running", "cancelling"]), + ) + row = await cur.fetchone() + return int(row["n"]) + + +async def abort_owned( + conn: AsyncConnection, + run_id: UUID, + owner_account_id: UUID, +) -> dict[str, Any] | None: + now = datetime.now(UTC) + async with conn.cursor() as cur: + await cur.execute( + f"""UPDATE strategy_evo_runs SET status=CASE WHEN status='queued' THEN 'aborted' +ELSE 'cancelling' END,finished_at=CASE WHEN status='queued' THEN %s ELSE finished_at END, +updated_at=%s WHERE run_id=%s AND owner_account_id=%s AND status=ANY(%s) RETURNING {_COLUMNS}""", + (now, now, run_id, owner_account_id, ["queued", "running"]), + ) + row = await cur.fetchone() + return dict(row) if row else None + + +async def reconcile_interrupted(conn: AsyncConnection) -> int: + """服务重启时保留 queued,收口可能已计费的 active run。""" + now = datetime.now(UTC) + async with conn.cursor() as cur: + await cur.execute( + """UPDATE strategy_evo_candidates c SET stage='completed',outcome='cancelled', +error_code='EVOLUTION_SERVICE_RESTARTED',error_message='service restarted during execution', +updated_at=%s FROM strategy_evo_runs r WHERE c.run_id=r.run_id AND c.outcome='pending' +AND r.status=ANY(%s)""", + (now, ["running", "cancelling"]), + ) + await cur.execute( + """UPDATE strategy_evo_runs SET status='aborted',active_stage='aborted', +finished_at=%s,updated_at=%s,failure_code='EVOLUTION_SERVICE_RESTARTED', +failure_message='service restarted during execution' +WHERE status=ANY(%s)""", + (now, now, ["running", "cancelling"]), + ) + return cur.rowcount diff --git a/services/evolver/src/inalpha_evolver/storage/runs.py b/services/evolver/src/inalpha_evolver/storage/runs.py new file mode 100644 index 00000000..7eef8a9e --- /dev/null +++ b/services/evolver/src/inalpha_evolver/storage/runs.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import json +from datetime import datetime +from typing import Any +from uuid import UUID, uuid4 + +from psycopg import AsyncConnection + +_COLUMNS = """run_id,owner_account_id,requested_by_sub,seed_strategy_id,budget,config,status, +llm_cost_usd,queued_at,started_at,updated_at,finished_at,venue,symbol,request_timeframe, +data_timeframe,engine_timeframe,requested_as_of,seed_source_snapshot,seed_source_hash, +seed_report_snapshot,baseline_snapshot,dataset_manifest,active_stage,failure_code,failure_message""" + + +async def insert_run( + conn: AsyncConnection, + *, + owner_account_id: UUID, + requested_by_sub: str, + idempotency_key: str, + request_hash: str, + seed_strategy_id: str, + seed_source: str, + seed_hash: str, + budget: int, + config: dict[str, Any], + queued_at: datetime, +) -> tuple[dict[str, Any], bool]: + run_id = uuid4() + async with conn.cursor() as cur: + await cur.execute( + f"""INSERT INTO strategy_evo_runs(run_id,owner_account_id,requested_by_sub, +seed_strategy_id,budget,config,status,idempotency_key,request_hash,queued_at,started_at, +venue,symbol,request_timeframe,data_timeframe,engine_timeframe,requested_as_of, +seed_source_snapshot,seed_source_hash) VALUES +(%s,%s,%s,%s,%s,%s,'queued',%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) +ON CONFLICT(owner_account_id,idempotency_key) DO NOTHING RETURNING {_COLUMNS},request_hash""", + ( + run_id, + owner_account_id, + requested_by_sub, + seed_strategy_id, + budget, + json.dumps(config, default=str), + idempotency_key, + request_hash, + queued_at, + queued_at, + config["venue"], + config["symbol"], + config["timeframe"], + config["timeframe"], + config["timeframe"], + config["as_of"], + seed_source, + seed_hash, + ), + ) + row = await cur.fetchone() + if row is not None: + return dict(row), True + await cur.execute( + f"SELECT {_COLUMNS},request_hash FROM strategy_evo_runs WHERE owner_account_id=%s AND idempotency_key=%s", + (owner_account_id, idempotency_key), + ) + existing = await cur.fetchone() + if existing is None: + raise RuntimeError("idempotent run disappeared") + return dict(existing), False + + +async def get_run( + conn: AsyncConnection, + run_id: UUID, + owner_account_id: UUID | None = None, +) -> dict[str, Any] | None: + sql = f"SELECT {_COLUMNS} FROM strategy_evo_runs WHERE run_id=%s" + params: list[Any] = [run_id] + if owner_account_id is not None: + sql += " AND owner_account_id=%s" + params.append(owner_account_id) + async with conn.cursor() as cur: + await cur.execute(sql, params) + row = await cur.fetchone() + return dict(row) if row else None + + +async def transition( + conn: AsyncConnection, + run_id: UUID, + *, + from_statuses: tuple[str, ...], + to_status: str, + values: dict[str, Any] | None = None, +) -> dict[str, Any] | None: + updates: dict[str, Any] = {"status": to_status, "updated_at": datetime.now().astimezone()} + updates.update(values or {}) + assignments = ",".join(f"{key}=%s" for key in updates) + params: list[Any] = [json.dumps(value) if isinstance(value, dict) else value for value in updates.values()] + params.extend([run_id, list(from_statuses)]) + async with conn.cursor() as cur: + await cur.execute( + f"UPDATE strategy_evo_runs SET {assignments} WHERE run_id=%s AND status=ANY(%s) RETURNING {_COLUMNS}", + params, + ) + row = await cur.fetchone() + return dict(row) if row else None diff --git a/services/evolver/tests/test_api_contract.py b/services/evolver/tests/test_api_contract.py new file mode 100644 index 00000000..c876bc7c --- /dev/null +++ b/services/evolver/tests/test_api_contract.py @@ -0,0 +1,119 @@ +"""Evolver API schema 与 presenter 单测。""" +from datetime import UTC, datetime, timedelta, timezone +from uuid import uuid4 + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from inalpha_evolver.api.presenters import candidate_response, run_response +from inalpha_evolver.api.request_hash import normalized_request +from inalpha_evolver.api.schemas import ( + EvolutionConfig, + RunStatusResponse, + StartRunRequest, +) + + +def _request(symbol: str = "BTCUSDT") -> StartRunRequest: + now = datetime(2026, 8, 12, 12, tzinfo=UTC) + return StartRunRequest( + config=EvolutionConfig( + venue="binance", + symbol=symbol, + timeframe="1h", + from_ts=now - timedelta(days=30), + as_of=now, + ) + ) + + +def test_request_hash_is_stable_and_payload_sensitive() -> None: + config_a, hash_a = normalized_request(_request()) + config_b, hash_b = normalized_request(_request()) + _config_c, hash_c = normalized_request(_request("ETHUSDT")) + + assert config_a == config_b + assert hash_a == hash_b + assert hash_a != hash_c + + +def test_invalid_window_is_rejected() -> None: + now = datetime(2026, 8, 12, 12, tzinfo=UTC) + with pytest.raises(ValueError): + EvolutionConfig( + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=now, + as_of=now, + ) + + +def test_run_presenter_converts_numeric_cost() -> None: + now = datetime(2026, 8, 12, 12, tzinfo=UTC) + response = run_response( + { + "run_id": uuid4(), + "seed_strategy_id": "sma_cross_v1", + "budget": 4, + "config": {}, + "status": "queued", + "llm_cost_usd": "0.1250", + "queued_at": now, + }, + summary={"attempted": 2, "succeeded": 1, "rejected": 1}, + ) + assert response.llm_cost_usd == pytest.approx(0.125) + assert response.attempted == 2 + + +def test_datetime_inputs_normalize_or_fail_without_type_error() -> None: + now = datetime.now(UTC) - timedelta(minutes=1) + config = EvolutionConfig( + venue="binance", symbol="BTCUSDT", timeframe="1h", + from_ts=(now - timedelta(days=1)).replace(tzinfo=None), + as_of=now.astimezone(timezone(timedelta(hours=9))), + ) + assert config.from_ts.tzinfo == UTC + assert config.as_of.tzinfo == UTC + with pytest.raises(ValueError, match="timezone-aware"): + EvolutionConfig( + venue="binance", symbol="BTCUSDT", timeframe="1h", + from_ts=now - timedelta(days=1), as_of=now.replace(tzinfo=None)) + + +def test_future_as_of_returns_http_422() -> None: + app = FastAPI() + + @app.post("/validate") + async def validate(body: StartRunRequest) -> dict[str, bool]: + return {"ok": bool(body)} + + future = datetime.now(UTC) + timedelta(minutes=1) + response = TestClient(app).post( + "/validate", + json={"config": {"venue": "binance", "symbol": "BTCUSDT", + "timeframe": "1h", + "from_ts": (future - timedelta(days=1)).isoformat(), + "as_of": future.isoformat()}}, + ) + assert response.status_code == 422 + assert "trusted current time" in response.text + + +def test_candidate_response_exposes_data_epoch() -> None: + candidate_id, run_id = uuid4(), uuid4() + response = candidate_response({ + "candidate_id": candidate_id, "run_id": run_id, "slot": 1, + "generation": 1, "stage": "evaluation", "outcome": "succeeded", + "data_epoch": 1_786_000_000_000, + }) + assert response.data_epoch == 1_786_000_000_000 + + +def test_run_dto_exposes_manifest_cutoff_and_lag() -> None: + manifest = RunStatusResponse.model_json_schema()["$defs"]["DatasetManifest"] + required = set(manifest["required"]) + assert {"latest_bar_ts", "cutoff_bar_ts", "freshness_lag_seconds", + "data_epoch", "backfill"} <= required diff --git a/services/evolver/tests/test_e2e.py b/services/evolver/tests/test_e2e.py index 71eaabce..e0597508 100644 --- a/services/evolver/tests/test_e2e.py +++ b/services/evolver/tests/test_e2e.py @@ -1,77 +1,78 @@ -"""E2E 测试 —— 完整闭环。 - -测试策略(E1 不接真实 DB/LLM): -1. POST /runs { budget: 2 } 返回 202 -2. 响应体含 run_id + 正确状态 -3. GET /runs/{id} 返回状态 -""" +"""Evolver API DB/auth 契约测试。""" from __future__ import annotations +import os +import time +from datetime import UTC, datetime, timedelta +from uuid import uuid4 + +import jwt import pytest from fastapi.testclient import TestClient +from inalpha_evolver.config import get_evolver_settings from inalpha_evolver.main import app - -@pytest.fixture -def client() -> TestClient: - return TestClient(app) +_SECRET = "evolver-test-secret-at-least-32-bytes-long" -def test_health_check(client: TestClient) -> None: - """健康检查端点。""" - response = client.get("/health") - assert response.status_code == 200 - data = response.json() - assert data["service"] == "inalpha-evolver" +def _headers(key: str | None = None) -> dict[str, str]: + token = jwt.encode( + {"sub": f"test:{uuid4()}", "exp": int(time.time()) + 3600}, + _SECRET, + algorithm="HS256", + ) + headers = {"Authorization": f"Bearer {token}"} + if key: + headers["Idempotency-Key"] = key + return headers -def test_start_run(client: TestClient) -> None: - """POST /runs 启动演化。""" - payload = { +@pytest.fixture(scope="module") +def client() -> TestClient: + os.environ["DATABASE_URL"] = os.environ.get( + "EVOLVER_TEST_DATABASE_URL", + "postgresql+psycopg://quant:devpass@localhost:5433/inalpha_evo_test", + ) + os.environ["JWT_SECRET"] = _SECRET + get_evolver_settings.cache_clear() + with TestClient(app) as value: + yield value + get_evolver_settings.cache_clear() + + +def _payload() -> dict: + now = datetime.now(UTC) + return { "seed_strategy_id": "sma_cross_v1", - "budget": 2, + "budget": 1, "config": { - "universe": ["BTCUSDT"], - "period_from": "2025-01-01", - "period_to": "2025-12-31", + "venue": "binance", + "symbol": "BTCUSDT", "timeframe": "1h", - "initial_cash": 10000.0, + "from_ts": (now - timedelta(days=1)).isoformat(), + "as_of": now.isoformat(), + "initial_cash": 10_000, }, } - response = client.post("/api/v1/runs", json=payload) - assert response.status_code == 202 - data = response.json() - assert "run_id" in data - assert data["status"] in ("completed", "failed") - assert data["budget"] == 2 - - -def test_start_run_minimal(client: TestClient) -> None: - """POST /runs 最少参数。""" - response = client.post("/api/v1/runs", json={}) - assert response.status_code == 202 - data = response.json() - assert data["status"] in ("completed", "failed") - assert data["budget"] == 4 # 默认 - - -def test_get_run(client: TestClient) -> None: - """GET /runs/{id} 返回正确状态。""" - # 先启动一个 run - resp = client.post("/api/v1/runs", json={"budget": 1}) - assert resp.status_code == 202 - run_id = resp.json()["run_id"] - - # 再查询 - resp2 = client.get(f"/api/v1/runs/{run_id}") - assert resp2.status_code == 200 - assert resp2.json()["run_id"] == run_id - - -def test_get_nonexistent_run(client: TestClient) -> None: - """GET /runs/{id} 不存在时返回 404。""" - import uuid - - resp = client.get(f"/api/v1/runs/{uuid.uuid4()}") - assert resp.status_code == 404 \ No newline at end of file + + +def test_business_endpoints_require_auth(client: TestClient) -> None: + assert client.get("/api/v1/runs").status_code == 401 + + +def test_start_requires_idempotency_key(client: TestClient) -> None: + assert client.post("/api/v1/runs", json=_payload(), headers=_headers()).status_code == 400 + + +def test_start_returns_queued_and_is_idempotent(client: TestClient) -> None: + key = f"api-test-{uuid4()}" + headers = _headers(key) + payload = _payload() + first = client.post("/api/v1/runs", json=payload, headers=headers) + second = client.post("/api/v1/runs", json=payload, headers=headers) + + assert first.status_code == 202 + assert first.json()["status"] == "queued" + assert second.status_code == 202 + assert second.json()["run_id"] == first.json()["run_id"] diff --git a/services/evolver/tests/test_frozen_bars.py b/services/evolver/tests/test_frozen_bars.py new file mode 100644 index 00000000..0bf6ab51 --- /dev/null +++ b/services/evolver/tests/test_frozen_bars.py @@ -0,0 +1,178 @@ +"""严格冻结行情加载器测试。""" +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest +from inalpha_shared.errors import ValidationError + +from inalpha_evolver.data.frozen_bars import FrozenBarsLoader + +_AS_OF = datetime(2026, 8, 12, 12, tzinfo=UTC) + + +class FakeDataClient: + def __init__( + self, + bars: list[dict[str, Any]], + *, + backfill_error: Exception | None = None, + ) -> None: + self.bars = bars + self.backfill_error = backfill_error + self.calls: list[tuple[str, dict[str, Any]]] = [] + + async def backfill_bars(self, **kwargs: Any) -> dict[str, Any]: + self.calls.append(("backfill", kwargs)) + if self.backfill_error: + raise self.backfill_error + return { + "venue": kwargs["venue"], + "symbol": kwargs["symbol"], + "timeframe": kwargs["timeframe"], + "bars_fetched": len(self.bars), + "bars_inserted": len(self.bars), + "from_ts": kwargs["from_ts"], + "to_ts": kwargs["to_ts"], + } + + async def get_bars(self, **kwargs: Any) -> list[dict[str, Any]]: + self.calls.append(("get", kwargs)) + return self.bars + + +def _hourly_bars(count: int = 3, *, end: datetime = _AS_OF) -> list[dict[str, Any]]: + start = end - timedelta(hours=count) + return [ + { + "venue": "binance", + "symbol": "BTCUSDT", + "timeframe": "1h", + "ts": (start + timedelta(hours=index)).isoformat(), + "open": 100.0 + index, + "high": 101.0 + index, + "low": 99.0 + index, + "close": 100.5 + index, + "volume": 10.0, + } + for index in range(count) + ] + + +@pytest.mark.asyncio +async def test_load_backfills_then_reads_without_fresh_fallback() -> None: + client = FakeDataClient(_hourly_bars()) + dataset = await FrozenBarsLoader(client).load( # type: ignore[arg-type] + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF, + ) + + assert [call[0] for call in client.calls] == ["backfill", "get"] + assert client.calls[1][1]["fresh"] is False + assert client.calls[1][1]["limit"] == 10_001 + assert dataset.manifest.bar_count == 3 + assert dataset.manifest.latest_bar_ts == _AS_OF - timedelta(hours=1) + assert dataset.manifest.cutoff_bar_ts == dataset.manifest.latest_bar_ts + assert dataset.manifest.data_epoch == int(dataset.manifest.latest_bar_ts.timestamp() * 1000) + assert dataset.manifest.backfill.bars_fetched == 3 + assert dataset.manifest.backfill.venue == "binance" + assert len(dataset.manifest.content_sha256) == 64 + + +@pytest.mark.asyncio +async def test_forming_bar_is_removed() -> None: + bars = _hourly_bars() + bars.append({**bars[-1], "ts": _AS_OF.isoformat()}) + client = FakeDataClient(bars) + dataset = await FrozenBarsLoader(client).load( # type: ignore[arg-type] + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF, + ) + assert dataset.manifest.bar_count == 3 + + +@pytest.mark.asyncio +async def test_stale_tail_fails_closed() -> None: + client = FakeDataClient(_hourly_bars(end=_AS_OF - timedelta(hours=1))) + with pytest.raises(ValidationError) as error: + await FrozenBarsLoader(client).load( # type: ignore[arg-type] + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF, + ) + assert error.value.code == "EVOLUTION_DATA_FRESHNESS_FAILED" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("mutation", ["duplicate", "nan", "bad_ohlc"]) +async def test_bad_bars_fail_closed(mutation: str) -> None: + bars = _hourly_bars() + if mutation == "duplicate": + bars[1]["ts"] = bars[0]["ts"] + elif mutation == "nan": + bars[1]["close"] = float("nan") + else: + bars[1]["high"] = 50.0 + client = FakeDataClient(bars) + with pytest.raises(ValidationError): + await FrozenBarsLoader(client).load( # type: ignore[arg-type] + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF, + ) + + +@pytest.mark.asyncio +async def test_middle_gap_fails_even_when_tail_is_fresh() -> None: + bars = _hourly_bars(4) + del bars[1] + with pytest.raises(ValidationError) as error: + await FrozenBarsLoader(FakeDataClient(bars)).load( # type: ignore[arg-type] + venue="binance", symbol="BTCUSDT", timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), as_of=_AS_OF) + assert error.value.code == "EVOLUTION_DATA_GAP_INVALID" + + +@pytest.mark.asyncio +async def test_equity_holiday_is_not_reported_as_gap() -> None: + as_of = datetime(2026, 7, 6, 21, tzinfo=UTC) + bars = [ + { + "venue": "yfinance", "symbol": "AAPL", "timeframe": "1d", + "ts": datetime(2026, 7, day, 4, tzinfo=UTC).isoformat(), + "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.5, + "volume": 10.0, + } + for day in (1, 2, 6) + ] + dataset = await FrozenBarsLoader(FakeDataClient(bars)).load( # type: ignore[arg-type] + venue="yfinance", symbol="AAPL", timeframe="1d", + from_ts=datetime(2026, 7, 1, tzinfo=UTC), as_of=as_of) + assert dataset.manifest.bar_count == 3 + + +@pytest.mark.asyncio +async def test_loader_rejects_naive_and_future_as_of() -> None: + loader = FrozenBarsLoader(FakeDataClient(_hourly_bars())) # type: ignore[arg-type] + with pytest.raises(ValidationError) as naive: + await loader.load(venue="binance", symbol="BTCUSDT", timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF.replace(tzinfo=None)) + assert naive.value.code == "EVOLUTION_DATETIME_NAIVE" + + future = datetime.now(UTC) + timedelta(minutes=1) + with pytest.raises(ValidationError) as ahead: + await loader.load(venue="binance", symbol="BTCUSDT", timeframe="1h", + from_ts=_AS_OF, as_of=future) + assert ahead.value.code == "EVOLUTION_AS_OF_IN_FUTURE" diff --git a/services/evolver/tests/test_frozen_bars_errors.py b/services/evolver/tests/test_frozen_bars_errors.py new file mode 100644 index 00000000..c2d85d99 --- /dev/null +++ b/services/evolver/tests/test_frozen_bars_errors.py @@ -0,0 +1,106 @@ +"""冻结行情加载错误与哈希测试。""" +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest +from inalpha_paper.data_client import DataServiceError +from inalpha_shared.errors import ValidationError + +from inalpha_evolver.data.frozen_bars import FrozenBarsLoader + +_AS_OF = datetime(2026, 8, 12, 12, tzinfo=UTC) + + +class FakeDataClient: + def __init__( + self, + bars: list[dict[str, Any]], + error: Exception | None = None, + ) -> None: + self.bars = bars + self.error = error + + async def backfill_bars(self, **kwargs: Any) -> dict[str, Any]: + if self.error: + raise self.error + return { + "venue": kwargs["venue"], + "symbol": kwargs["symbol"], + "timeframe": kwargs["timeframe"], + "bars_fetched": len(self.bars), + "bars_inserted": len(self.bars), + "from_ts": kwargs["from_ts"], + "to_ts": kwargs["to_ts"], + } + + async def get_bars(self, **_kwargs: Any) -> list[dict[str, Any]]: + return self.bars + + +def _bars() -> list[dict[str, Any]]: + return [ + { + "venue": "binance", + "symbol": "BTCUSDT", + "timeframe": "1h", + "ts": (_AS_OF - timedelta(hours=3 - index)).isoformat(), + "open": 100.0 + index, + "high": 101.0 + index, + "low": 99.0 + index, + "close": 100.5 + index, + "volume": 10.0, + } + for index in range(3) + ] + + +async def _load(client: FakeDataClient): + return await FrozenBarsLoader(client).load( # type: ignore[arg-type] + venue="binance", + symbol="BTCUSDT", + timeframe="1h", + from_ts=_AS_OF - timedelta(days=1), + as_of=_AS_OF, + ) + + +@pytest.mark.asyncio +async def test_backfill_failure_never_reads_cached_bars() -> None: + client = FakeDataClient( + _bars(), + DataServiceError("failed", code="DATA_BACKFILL_FAILED"), + ) + with pytest.raises(ValidationError) as error: + await _load(client) + assert error.value.code == "EVOLUTION_DATA_FRESHNESS_FAILED" + + +@pytest.mark.asyncio +async def test_dataset_limit_is_rejected() -> None: + client = FakeDataClient(_bars() * 3334) + with pytest.raises(ValidationError) as error: + await _load(client) + assert error.value.code == "EVOLUTION_DATA_LIMIT_EXCEEDED" + + +@pytest.mark.asyncio +async def test_hash_is_stable_and_sensitive_to_ohlcv() -> None: + first = await _load(FakeDataClient(_bars())) + second = await _load(FakeDataClient(_bars())) + changed = _bars() + changed[0]["volume"] = 11.0 + third = await _load(FakeDataClient(changed)) + + assert first.manifest.content_sha256 == second.manifest.content_sha256 + assert first.manifest.content_sha256 != third.manifest.content_sha256 + + +@pytest.mark.asyncio +async def test_identity_mismatch_is_rejected() -> None: + bars = _bars() + bars[0]["symbol"] = "ETHUSDT" + with pytest.raises(ValidationError) as error: + await _load(FakeDataClient(bars)) + assert error.value.code == "EVOLUTION_DATA_IDENTITY_MISMATCH" diff --git a/services/evolver/tests/test_frozen_evaluator.py b/services/evolver/tests/test_frozen_evaluator.py new file mode 100644 index 00000000..36e5e00b --- /dev/null +++ b/services/evolver/tests/test_frozen_evaluator.py @@ -0,0 +1,115 @@ +"""冻结数据集 evaluator 测试。""" +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest +from inalpha_paper.engine.report import BacktestReport +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.model.data import Bar + +from inalpha_evolver.data.manifest import DatasetManifest, FrozenDataset +from inalpha_evolver.evaluator.frozen import FrozenDatasetEvaluator + + +def _dataset() -> FrozenDataset: + instrument = InstrumentId(symbol="BTCUSDT", venue="binance") + start = datetime(2026, 1, 1, tzinfo=UTC) + bars = tuple( + Bar( + instrument_id=instrument, + timeframe="1h", + open=100.0 + index, + high=101.0 + index, + low=99.0 + index, + close=100.5 + index, + volume=10.0, + ts_event=int((start + timedelta(hours=index)).timestamp() * 1e9), + ts_init=int((start + timedelta(hours=index)).timestamp() * 1e9), + ) + for index in range(40) + ) + manifest = DatasetManifest( + venue="binance", + symbol="BTCUSDT", + requested_timeframe="1h", + data_timeframe="1h", + canonical_timeframe="1h", + requested_from=start, + requested_as_of=start + timedelta(hours=41), + effective_from=start, + effective_to=start + timedelta(hours=39), + latest_bar_ts=start + timedelta(hours=39), + cutoff_bar_ts=start + timedelta(hours=39), + data_epoch=int((start + timedelta(hours=39)).timestamp() * 1000), + bar_count=40, + freshness_lag_seconds=0, + annualization_periods=8760, + calendar_code=None, + content_sha256="a" * 64, + backfill={ + "venue": "binance", + "symbol": "BTCUSDT", + "timeframe": "1h", + "bars_fetched": 40, + "bars_inserted": 40, + "from_ts": start, + "to_ts": start + timedelta(hours=41), + }, + ) + return FrozenDataset(bars=bars, manifest=manifest) + + +class FakeRunner: + async def __call__(self, **kwargs: Any) -> BacktestReport: + bars = kwargs["bars"] + curve = [(bar.ts_event, 10_000.0 + index) for index, bar in enumerate(bars)] + return BacktestReport( + initial_cash=10_000.0, + final_equity=curve[-1][1], + total_return_pct=0.39, + num_trades=6, + total_fees=1.0, + num_bars_processed=len(bars), + period_start=_dataset().manifest.effective_from, + period_end=_dataset().manifest.effective_to, + positions={}, + sharpe=1.0, + max_drawdown_pct=2.0, + equity_curve=curve, + ) + + +@pytest.mark.asyncio +async def test_frozen_evaluator_uses_manifest_epoch_and_snapshot() -> None: + evaluator = FrozenDatasetEvaluator( + dataset=_dataset(), + runner=FakeRunner(), # type: ignore[arg-type] + ) + source = """ +class TestStrategy(Strategy): + def __init__(self, name, clock, msgbus, instrument_id, timeframe="1h"): + super().__init__(name, clock, msgbus) + def on_bar(self, bar): + pass +""" + result = await evaluator.evaluate(source) + + assert result.data_epoch == int(_dataset().manifest.latest_bar_ts.timestamp() * 1000) + assert result.report["schema_version"] == "e1.report.v1" + assert "equity_curve" not in result.report + assert result.fitness == result.report["fitness"] + + +@pytest.mark.asyncio +async def test_frozen_evaluator_builds_baseline_snapshot() -> None: + evaluator = FrozenDatasetEvaluator( + dataset=_dataset(), + runner=FakeRunner(), # type: ignore[arg-type] + ) + snapshot = await evaluator.evaluate_baseline() + + assert snapshot["schema_version"] == "e1.report.v1" + assert snapshot["annualization_periods"] == 8760.0 + assert "equity_curve" not in snapshot diff --git a/services/evolver/tests/test_health.py b/services/evolver/tests/test_health.py new file mode 100644 index 00000000..233c7e89 --- /dev/null +++ b/services/evolver/tests/test_health.py @@ -0,0 +1,31 @@ +"""Evolver health 对 dispatcher 状态的守门测试。""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +from fastapi.responses import JSONResponse + +from inalpha_evolver.main import health + + +@pytest.mark.asyncio +async def test_health_is_unhealthy_when_dispatcher_failed() -> None: + manager = SimpleNamespace(healthy=False, unhealthy_reason="dispatcher failed") + request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(evolution_manager=manager))) + + response = await health(request) # type: ignore[arg-type] + + assert isinstance(response, JSONResponse) + assert response.status_code == 503 + + +@pytest.mark.asyncio +async def test_health_is_ok_when_manager_is_healthy() -> None: + manager = SimpleNamespace(healthy=True, unhealthy_reason=None) + request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(evolution_manager=manager))) + + response = await health(request) # type: ignore[arg-type] + + assert response == {"status": "ok", "service": "inalpha-evolver"} diff --git a/services/evolver/tests/test_mutator.py b/services/evolver/tests/test_mutator.py index c751b7cd..18dd75a4 100644 --- a/services/evolver/tests/test_mutator.py +++ b/services/evolver/tests/test_mutator.py @@ -1,21 +1,11 @@ -"""diff_applier 单元测试。 - -测试策略: -1. 精确匹配(行号完全准确) -2. fuzz match(±1/2/3 行偏移) -3. 不可恢复的 diff(行号偏移过大 / 内容不匹配) -4. 空 diff / 无效 diff -5. 多 hunk diff -6. 新增/删除/修改 三种变更类型 -""" +"""严格 unified diff 应用器测试。""" + from __future__ import annotations import pytest from inalpha_evolver.mutator.diff_applier import DiffApplyError, apply_diff, apply_diff_strict -# ── 测试用例 ────────────────────────────────────────────────────── - _SOURCE = """ class Foo: def bar(self): @@ -27,16 +17,14 @@ def bar(self): def _diff(*lines: str) -> str: - """构造带正确行尾分隔的 diff 字符串。""" - return "\n".join(lines) + "\n\n" + return "\n".join(lines) + "\n" -def test_exact_match() -> None: - """精确匹配:行号完全准确,直接应用。""" - diff = _diff( +def _replace_y(*, old_start: int = 2, old_count: int = 5) -> str: + return _diff( "--- a/strategy.py", "+++ b/strategy.py", - "@@ -2,6 +2,6 @@", + f"@@ -{old_start},{old_count} +2,5 @@", " def bar(self):", " x = 1", "- y = 2", @@ -44,174 +32,100 @@ def test_exact_match() -> None: " z = 3", " return x + y + z", ) - result = apply_diff(_SOURCE, diff) - # 注意 'y = 2' 是 'y = 20' 的子串,用 ' y = 2\\n' 做精确断言 - assert "y = 20" in result - assert " y = 2\n" not in result -def test_fuzz_plus_2() -> None: - """fuzz +2 行偏移:行号偏移 2 行仍能匹配。""" - # 原始行号 2 → 实际偏移到 4 - diff = _diff( - "--- a/strategy.py", - "+++ b/strategy.py", - "@@ -4,6 +4,6 @@", - " def bar(self):", - " x = 1", - "- y = 2", - "+ y = 20", - " z = 3", - " return x + y + z", - ) - result = apply_diff(_SOURCE, diff, max_fuzz=3) - assert "y = 20" in result - - -def test_fuzz_minus_1() -> None: - """fuzz -1 行偏移。""" - diff = _diff( - "--- a/strategy.py", - "+++ b/strategy.py", - "@@ -1,6 +1,6 @@", - " def bar(self):", - " x = 1", - "- y = 2", - "+ y = 20", - " z = 3", - " return x + y + z", - ) - result = apply_diff(_SOURCE, diff, max_fuzz=3) +def test_exact_match_is_applied() -> None: + result = apply_diff(_SOURCE, _replace_y(), max_fuzz=99) assert "y = 20" in result + assert " y = 2\n" not in result -def test_fuzz_too_large() -> None: - """fuzz 超出 max_fuzz 但全文件扫描仍能找到(max_fuzz>0 时启用全扫描)。""" - diff = _diff( - "--- a/strategy.py", - "+++ b/strategy.py", - "@@ -10,6 +10,6 @@", - " def bar(self):", - " x = 1", - "- y = 2", - "+ y = 20", - " z = 3", - " return x + y + z", - ) - # max_fuzz=2 > 0 → 启用全文件滑动窗口搜索 → 能匹配 - result = apply_diff(_SOURCE, diff, max_fuzz=2) +def test_markdown_diff_fence_is_supported() -> None: + result = apply_diff(_SOURCE, f"```diff\n{_replace_y()}```") assert "y = 20" in result -def test_strict_fails_with_offset() -> None: - """严格模式(fuzz=0)行号偏移时失败。""" +def test_multi_hunk_is_applied_atomically() -> None: diff = _diff( "--- a/strategy.py", "+++ b/strategy.py", - "@@ -3,6 +3,6 @@", - " def bar(self):", - " x = 1", - "- y = 2", - "+ y = 20", - " z = 3", - " return x + y + z", - ) - with pytest.raises(DiffApplyError): - apply_diff_strict(_SOURCE, diff) - - -def test_empty_diff() -> None: - """空 diff → 返回原源码。""" - result = apply_diff(_SOURCE, "") - assert result == _SOURCE - - -def test_whitespace_diff() -> None: - """仅空白 diff → 返回原源码。""" - result = apply_diff(_SOURCE, " \n \n") - assert result == _SOURCE - - -def test_add_line() -> None: - """新增行。""" - diff = _diff( - "--- a/strategy.py", - "+++ b/strategy.py", - "@@ -2,6 +2,7 @@", + "@@ -2,3 +2,4 @@", " def bar(self):", " x = 1", "+ a = 0", " y = 2", + "@@ -5,2 +6,3 @@", " z = 3", + "+ w = 4", " return x + y + z", ) - result = apply_diff(_SOURCE, diff) + result = apply_diff_strict(_SOURCE, diff) assert "a = 0" in result + assert "w = 4" in result -def test_delete_line() -> None: - """删除行。""" +def test_insert_at_start() -> None: diff = _diff( "--- a/strategy.py", "+++ b/strategy.py", - "@@ -2,5 +2,4 @@", - " def bar(self):", - " x = 1", - "- y = 2", - " z = 3", - " return x + y + z", + "@@ -0,0 +1,1 @@", + "+# generated", ) - result = apply_diff(_SOURCE, diff) - assert " y = 2\n" not in result + assert apply_diff(_SOURCE, diff).startswith("# generated\nclass Foo") -def test_multi_hunk() -> None: - """多 hunk diff。""" +def test_delete_line() -> None: diff = _diff( "--- a/strategy.py", "+++ b/strategy.py", - "@@ -2,4 +2,5 @@", - " def bar(self):", + "@@ -3,3 +3,2 @@", " x = 1", - "+ a = 0", - " y = 2", - "", - "@@ -5,3 +6,4 @@", + "- y = 2", " z = 3", - "+ w = 4", - " return x + y + z", ) - result = apply_diff(_SOURCE, diff) - assert "a = 0" in result - assert "w = 4" in result + assert "y = 2" not in apply_diff(_SOURCE, diff) + + +@pytest.mark.parametrize( + "diff", + [ + _replace_y(old_start=3), + _replace_y(old_count=6), + _replace_y().replace("--- a/strategy.py", "--- a/other.py"), + _replace_y().replace("+++ b/strategy.py", "+++ b/other.py"), + _replace_y() + _diff("--- a/strategy.py", "+++ b/strategy.py"), + "explanation\n" + _replace_y(), + _replace_y().replace("@@ -2,5 +2,5 @@", "@@ malformed @@"), + _replace_y().replace(" z = 3", "? z = 3"), + ], +) +def test_malformed_or_ambiguous_diff_is_rejected(diff: str) -> None: + with pytest.raises(DiffApplyError): + apply_diff(_SOURCE, diff, max_fuzz=3) -def test_nonexistent_hunk() -> None: - """hunk 行号完全错误但内容匹配 → 全扫描能修复位置。""" +def test_any_failed_hunk_rejects_the_whole_patch() -> None: diff = _diff( "--- a/strategy.py", "+++ b/strategy.py", - "@@ -99,6 +99,6 @@", + "@@ -2,2 +2,2 @@", " def bar(self):", - " x = 1", - "- y = 2", - "+ y = 20", - " z = 3", - " return x + y + z", + "- x = 1", + "+ x = 10", + "@@ -5,2 +5,2 @@", + " wrong = 3", + "- return x + y + z", + "+ return 0", ) - # max_fuzz=3 > 0 → 全扫描能匹配 → 成功应用 - result = apply_diff(_SOURCE, diff, max_fuzz=3) - assert "y = 20" in result + with pytest.raises(DiffApplyError): + apply_diff(_SOURCE, diff) + assert "x = 10" not in _SOURCE -def test_parse_error() -> None: - """无效 diff 格式 → 抛异常。""" - with pytest.raises(DiffApplyError): - apply_diff(_SOURCE, "这不是一个有效的 diff\n") +def test_empty_diff_keeps_source() -> None: + assert apply_diff(_SOURCE, " \n") == _SOURCE -def test_no_hunks() -> None: - """只有 header 没有 hunk → 抛异常(解析失败或空 patch)。""" - diff = _diff("--- a/strategy.py", "+++ b/strategy.py") +def test_header_without_hunk_is_rejected() -> None: with pytest.raises(DiffApplyError): - apply_diff(_SOURCE, diff) \ No newline at end of file + apply_diff(_SOURCE, _diff("--- a/strategy.py", "+++ b/strategy.py")) diff --git a/services/evolver/tests/test_population.py b/services/evolver/tests/test_population.py index 32b40668..e0cbda99 100644 --- a/services/evolver/tests/test_population.py +++ b/services/evolver/tests/test_population.py @@ -3,8 +3,6 @@ from uuid import uuid4 -import pytest - from inalpha_evolver.population import Candidate, EvolutionRun diff --git a/services/evolver/tests/test_runtime_finalizer.py b/services/evolver/tests/test_runtime_finalizer.py new file mode 100644 index 00000000..28558493 --- /dev/null +++ b/services/evolver/tests/test_runtime_finalizer.py @@ -0,0 +1,60 @@ +"""终态写入重试测试。""" + +from __future__ import annotations + +from types import SimpleNamespace +from uuid import uuid4 + +import pytest + +from inalpha_evolver.runtime.finalizer import execute_managed + + +class _Connection: + async def __aenter__(self): + return object() + + async def __aexit__(self, *_args): + return None + + +@pytest.mark.asyncio +async def test_finalizer_retries_transient_db_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + attempts = 0 + unhealthy: list[str] = [] + + async def fail_run(*_args, **_kwargs): + raise RuntimeError("run failed") + + async def close_pending(*_args, **_kwargs): + nonlocal attempts + attempts += 1 + if attempts == 1: + raise RuntimeError("db unavailable") + return 0 + + async def transition(*_args, **_kwargs): + return {} + + async def no_delay(_seconds): + return None + + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.execute_run", fail_run) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.get_conn", lambda: _Connection()) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.candidates.close_pending", close_pending) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.runs.transition", transition) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.asyncio.sleep", no_delay) + + await execute_managed( + {"run_id": uuid4()}, + mutator=object(), + settings=SimpleNamespace(evolver_run_timeout_s=10), + should_stop=lambda: False, + on_error=unhealthy.append, + on_success=lambda: None, + ) + + assert attempts == 2 + assert unhealthy and "db unavailable" in unhealthy[0] diff --git a/services/evolver/tests/test_runtime_generation.py b/services/evolver/tests/test_runtime_generation.py new file mode 100644 index 00000000..c54eb54f --- /dev/null +++ b/services/evolver/tests/test_runtime_generation.py @@ -0,0 +1,35 @@ +"""seed/baseline 并行评估收口测试。""" + +from __future__ import annotations + +import asyncio + +import pytest + +from inalpha_evolver.runtime.generation import _evaluate_seed_and_baseline + + +class _FailingEvaluator: + def __init__(self) -> None: + self.baseline_cancelled = asyncio.Event() + + async def evaluate(self, _source: str): + await asyncio.sleep(0) + raise RuntimeError("seed failed") + + async def evaluate_baseline(self): + try: + await asyncio.sleep(10) + except asyncio.CancelledError: + self.baseline_cancelled.set() + raise + + +@pytest.mark.asyncio +async def test_seed_failure_cancels_and_awaits_baseline() -> None: + evaluator = _FailingEvaluator() + + with pytest.raises(RuntimeError, match="seed failed"): + await _evaluate_seed_and_baseline(evaluator, "source") # type: ignore[arg-type] + + assert evaluator.baseline_cancelled.is_set() diff --git a/services/evolver/tests/test_runtime_helpers.py b/services/evolver/tests/test_runtime_helpers.py new file mode 100644 index 00000000..0b57e963 --- /dev/null +++ b/services/evolver/tests/test_runtime_helpers.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from uuid import uuid4 + +import pytest +from fastapi import HTTPException + +from inalpha_evolver.api.cursor import decode_cursor, encode_cursor +from inalpha_evolver.runtime.executor import _parse_config + + +def test_cursor_round_trip() -> None: + queued_at = datetime(2026, 8, 13, 5, 30, tzinfo=UTC) + run_id = uuid4() + assert decode_cursor(encode_cursor(queued_at, run_id)) == (queued_at, run_id) + + +def test_invalid_cursor_is_bad_request() -> None: + with pytest.raises(HTTPException) as caught: + decode_cursor("not-a-cursor") + assert caught.value.status_code == 400 + + +def test_parse_config_restores_datetime() -> None: + parsed = _parse_config( + { + "from_ts": "2026-08-01T00:00:00Z", + "as_of": "2026-08-13T00:00:00+00:00", + "venue": "binance", + } + ) + assert parsed["from_ts"] == datetime(2026, 8, 1, tzinfo=UTC) + assert parsed["as_of"] == datetime(2026, 8, 13, tzinfo=UTC) + assert parsed["venue"] == "binance" diff --git a/services/evolver/tests/test_runtime_manager.py b/services/evolver/tests/test_runtime_manager.py new file mode 100644 index 00000000..894c0835 --- /dev/null +++ b/services/evolver/tests/test_runtime_manager.py @@ -0,0 +1,126 @@ +"""EvolutionRunManager 状态收口与竞态测试。""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from uuid import uuid4 + +import pytest + +from inalpha_evolver.runtime.dispatcher import dispatch_runs +from inalpha_evolver.runtime.manager import EvolutionRunManager + + +class FakeContext: + async def __aenter__(self): + return object() + + async def __aexit__(self, *_args): + return None + + +def _manager(*, timeout: float = 10.0) -> EvolutionRunManager: + return EvolutionRunManager( + mutator=object(), + settings=SimpleNamespace( + evolver_max_running_runs=1, + evolver_run_timeout_s=timeout, + ), # type: ignore[arg-type] + ) + + +def _patch_finalizer(monkeypatch: pytest.MonkeyPatch, calls: list) -> None: + async def close_pending(*_args, **kwargs): + calls.append(("slots", kwargs)) + return 1 + + async def transition(*_args, **kwargs): + calls.append(("run", kwargs)) + return {} + + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.get_conn", lambda: FakeContext()) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.candidates.close_pending", close_pending) + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.runs.transition", transition) + + +@pytest.mark.asyncio +async def test_execute_failure_closes_slots_and_run(monkeypatch: pytest.MonkeyPatch) -> None: + manager = _manager() + calls: list = [] + + async def fail(*_args, **_kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.execute_run", fail) + _patch_finalizer(monkeypatch, calls) + await manager._execute({"run_id": uuid4()}) + + assert calls[0][0] == "slots" + assert calls[1][1]["to_status"] == "failed" + assert calls[1][1]["values"]["failure_code"] == "EVOLUTION_INTERNAL_ERROR" + + +@pytest.mark.asyncio +async def test_cancelled_run_becomes_aborted(monkeypatch: pytest.MonkeyPatch) -> None: + manager = _manager() + calls: list = [] + + async def cancel(*_args, **_kwargs): + raise asyncio.CancelledError + + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.execute_run", cancel) + _patch_finalizer(monkeypatch, calls) + with pytest.raises(asyncio.CancelledError): + await manager._execute({"run_id": uuid4()}) + + assert calls[1][1]["to_status"] == "aborted" + + +@pytest.mark.asyncio +async def test_repeated_abort_only_cancels_once() -> None: + manager = _manager() + run_id = uuid4() + task = asyncio.create_task(asyncio.sleep(10)) + manager.tasks[run_id] = task # type: ignore[assignment] + + await manager.abort(run_id) + await manager.abort(run_id) + + assert task.cancelling() == 1 + await asyncio.gather(task, return_exceptions=True) + + +@pytest.mark.asyncio +async def test_dispatch_error_releases_permit(monkeypatch: pytest.MonkeyPatch) -> None: + manager = _manager() + + async def fail_claim(_conn): + raise RuntimeError("db down") + + async def stop_after_error(_delay): + manager.closing = True + + monkeypatch.setattr("inalpha_evolver.runtime.dispatcher.get_conn", lambda: FakeContext()) + monkeypatch.setattr("inalpha_evolver.runtime.dispatcher.run_queries.claim_next", fail_claim) + monkeypatch.setattr("inalpha_evolver.runtime.dispatcher.asyncio.sleep", stop_after_error) + + await dispatch_runs(manager) + + assert manager.semaphore._value == 1 + assert manager.unhealthy_reason and "db down" in manager.unhealthy_reason + + +@pytest.mark.asyncio +async def test_run_total_deadline_is_persisted(monkeypatch: pytest.MonkeyPatch) -> None: + manager = _manager(timeout=0.001) + calls: list = [] + + async def hang(*_args, **_kwargs): + await asyncio.sleep(10) + + monkeypatch.setattr("inalpha_evolver.runtime.finalizer.execute_run", hang) + _patch_finalizer(monkeypatch, calls) + await manager._execute({"run_id": uuid4()}) + + assert calls[1][1]["values"]["failure_code"] == "EVOLUTION_RUN_TIMEOUT" diff --git a/services/evolver/tests/test_storage_integration.py b/services/evolver/tests/test_storage_integration.py new file mode 100644 index 00000000..fc75e46a --- /dev/null +++ b/services/evolver/tests/test_storage_integration.py @@ -0,0 +1,60 @@ +"""Evolver DB storage 集成测试。""" +from __future__ import annotations + +import hashlib +import os +from datetime import UTC, datetime, timedelta +from uuid import uuid4 + +import pytest +from inalpha_shared.db import close_pool, get_conn, init_pool + +from inalpha_evolver.governor.seed import SEED_STRATEGY_CODE +from inalpha_evolver.storage import candidates, run_queries, runs + + +@pytest.mark.asyncio +async def test_run_idempotency_owner_scope_and_slot() -> None: + url = os.environ.get( + "EVOLVER_TEST_DATABASE_URL", + "postgresql+psycopg://quant:devpass@localhost:5433/inalpha_evo_test", + ) + await init_pool(url) + try: + owner = uuid4() + other = uuid4() + now = datetime.now(UTC) + config = { + "venue": "binance", + "symbol": "BTCUSDT", + "timeframe": "1h", + "from_ts": now - timedelta(days=1), + "as_of": now, + "initial_cash": 10_000.0, + } + kwargs = { + "owner_account_id": owner, + "requested_by_sub": "storage-test", + "idempotency_key": f"test-{uuid4()}", + "request_hash": "hash-a", + "seed_strategy_id": "sma_cross_v1", + "seed_source": SEED_STRATEGY_CODE, + "seed_hash": hashlib.sha256(SEED_STRATEGY_CODE.encode()).hexdigest(), + "budget": 2, + "config": config, + "queued_at": now, + } + async with get_conn() as conn: + row, created = await runs.insert_run(conn, **kwargs) + repeated, created_again = await runs.insert_run(conn, **kwargs) + assert created is True + assert created_again is False + assert repeated["run_id"] == row["run_id"] + assert await runs.get_run(conn, row["run_id"], other) is None + claimed = await run_queries.claim_next(conn) + assert claimed and claimed["status"] == "running" + slot = await candidates.insert_slot(conn, row["run_id"], 0, "hint") + assert slot["slot"] == 0 + assert await candidates.list_candidates(conn, row["run_id"], other) == [] + finally: + await close_pool() diff --git a/services/evolver/uv.lock b/services/evolver/uv.lock new file mode 100644 index 00000000..d73ab7da --- /dev/null +++ b/services/evolver/uv.lock @@ -0,0 +1,2012 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "alembic" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" }, +] + +[[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 = "anthropic" +version = "0.121.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ca/3cb2c20ee729736fbd4546d5d8b67e818288529fe70cb7a80dbf80aef70b/anthropic-0.121.0.tar.gz", hash = "sha256:e79d6e08ab3376602fc9a70d4d5ea3540817c76cf7e16658bed790834e1833d6", size = 1013292, upload-time = "2026-08-07T17:11:07.241Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/91/b3d41643f1f639927e8c5fb02c3bd8bffe6f1f29e219b3bd4c61e267b15c/anthropic-0.121.0-py3-none-any.whl", hash = "sha256:6048713fa441e59e1cba8363171cd2a86273b25bd213e9c7ac70a523af88b011", size = 1035493, upload-time = "2026-08-07T17:11:08.508Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, + { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, + { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, + { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, + { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +] + +[[package]] +name = "backtester-mcp" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "duckdb" }, + { name = "numba" }, + { name = "numpy" }, + { name = "optuna" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/45/0796f2ee42a532103a470832cb5a110ebfac23500fc7be416dbd644d1c75/backtester_mcp-0.1.0.tar.gz", hash = "sha256:afde2249ff350765cb608629acefaf24999a0f9b8d0a2462b2610504eb2c230d", size = 754595, upload-time = "2026-04-14T15:55:59.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/35/b4b752e07209a9e61dafd65a5be4666a599dfeba4a95a66174ff6546144e/backtester_mcp-0.1.0-py3-none-any.whl", hash = "sha256:8c7711829dc16ad04a5cbf4b91010fe2b777014c9d6af8503725999fab6ad6ec", size = 35687, upload-time = "2026-04-14T15:55:57.43Z" }, +] + +[[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 = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[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 = "colorlog" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "duckdb" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/19/e57151753576373c6696a12022648546cca6038e8833fda2908ee2342d9b/duckdb-1.5.5.tar.gz", hash = "sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238", size = 18066741, upload-time = "2026-07-22T10:55:17.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/40/2e05d324400fdaa5656c9f48d6298da421cb034d85e509fa0e6e325cf04b/duckdb-1.5.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e", size = 32753858, upload-time = "2026-07-22T10:54:05.514Z" }, + { url = "https://files.pythonhosted.org/packages/79/15/5ceb58ffb5bb8a62b3fd7abb39c41467cdf94850ece02e6d88664dfc75ce/duckdb-1.5.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7", size = 17368293, upload-time = "2026-07-22T10:54:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5c/bf02da0b354fe83cca4f95a4fbf762181af466f7d551ab2a093f7698882a/duckdb-1.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae", size = 15509131, upload-time = "2026-07-22T10:54:12.228Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a9/5f1f09da421d8e930e0b063d11c1b3f90363f40ede74438cd188afdd13a2/duckdb-1.5.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6", size = 19391959, upload-time = "2026-07-22T10:54:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/6549769f158126fa64fd6c1ac2eb59a18282146c939867a3eb31b7c1db07/duckdb-1.5.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1", size = 21510909, upload-time = "2026-07-22T10:54:19.681Z" }, + { url = "https://files.pythonhosted.org/packages/af/b7/5753b41d3124838f868f9f523362812d9fc45409e9e4dd70dcbb0a25826e/duckdb-1.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f", size = 13168544, upload-time = "2026-07-22T10:54:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/44b679c7d46245f8398feae7edac959d1b83d4eb143e25b3fce0630b78bd/duckdb-1.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282", size = 13988684, upload-time = "2026-07-22T10:54:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/47/37/4a38116e7700720fd152c666292214fd3abdf916496991296d8d1f66efbf/duckdb-1.5.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0", size = 32754294, upload-time = "2026-07-22T10:54:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/7d392f1ba1eee0eaf4ab4c8c7a604bfe3536cd63f979cf5c98798664f807/duckdb-1.5.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366", size = 17368211, upload-time = "2026-07-22T10:54:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a5/0a6f4fa60562faa615e55e15bd1953a2f2b17a8edd8105e5cda215e43457/duckdb-1.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c", size = 15509136, upload-time = "2026-07-22T10:54:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/023c89f51978545b9fab318581bba0c457a58e7530d2d933e54ae7d8647c/duckdb-1.5.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a", size = 19392147, upload-time = "2026-07-22T10:54:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/41bef391fb8b23dbc133c9f2ba016e7a7a8124513d2cc1b430f1897d87e4/duckdb-1.5.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053", size = 21511060, upload-time = "2026-07-22T10:54:42.788Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/c44dfc1f924ac29b3252dc1b91393c01d009dbfe9f8ed33f10b986151bd1/duckdb-1.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28", size = 13168028, upload-time = "2026-07-22T10:54:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/591384b2cd59abddd6f5dc175e60374f9abae6064429f0c4402854c10f44/duckdb-1.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de", size = 13989955, upload-time = "2026-07-22T10:54:49.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/12c65bfa2d2605b81981b264788891bcf11ec72227889554cead5d8d13b9/duckdb-1.5.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2", size = 32761946, upload-time = "2026-07-22T10:54:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/b9/46/682ce155f17e0d2822d4f13ee3db9ca4b5b7c2da61b841b2629035e1f4bc/duckdb-1.5.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8", size = 17375069, upload-time = "2026-07-22T10:54:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/39/ce/a24bcbd3289c8f305a430759c5fc12242740b4af3e17f7593f3a34e333d2/duckdb-1.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151", size = 15519791, upload-time = "2026-07-22T10:55:00.681Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/3a01afbc615c1d418c0de58a6b68ac5ce2a8563232c0464bfbc2ce552398/duckdb-1.5.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece", size = 19398251, upload-time = "2026-07-22T10:55:04.168Z" }, + { url = "https://files.pythonhosted.org/packages/a1/43/3a5e81d1728f4d234c79bfe385808ee7c04834f7c37a4b5c257459c25614/duckdb-1.5.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe", size = 21513851, upload-time = "2026-07-22T10:55:07.864Z" }, + { url = "https://files.pythonhosted.org/packages/91/41/fc7c829172c60ca22485251eab285f4f1a0d87b486a024c726f21471d86e/duckdb-1.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353", size = 13691858, upload-time = "2026-07-22T10:55:11.181Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/95d9216b79e9273689d7ebce125a54503ed0c9bd7da931f0265888e99779/duckdb-1.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433", size = 14470180, upload-time = "2026-07-22T10:55:14.445Z" }, +] + +[[package]] +name = "exchange-calendars" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "korean-lunar-calendar" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyluach" }, + { name = "toolz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/73/460b0ece4e7444e3098b7738974c822787e134578d069d3a88b4be17d50a/exchange_calendars-4.13.2.tar.gz", hash = "sha256:a9459425dd64142cd54fbc639847403c7e0c33d60fbc326c94fc1d6bd127f002", size = 4137321, upload-time = "2026-03-10T03:24:38.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/4c/0469b40057bc9f8d9594dcc6024202626b981ae4b52dfcd304552e8e1c3a/exchange_calendars-4.13.2-py3-none-any.whl", hash = "sha256:fc5a2ad0d61b5c3a6539a3061cd4cbb55c59f4a903455cec7926e4b798919996", size = 213306, upload-time = "2026-03-10T03:24:37.055Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + +[[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 = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "inalpha-evolver" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "inalpha-paper" }, + { name = "inalpha-shared" }, + { name = "inalpha-shared-llm" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "unidiff" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.0" }, + { name = "inalpha-paper", editable = "../paper" }, + { name = "inalpha-shared", editable = "../_shared" }, + { name = "inalpha-shared-llm", editable = "../_shared/llm" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.0" }, + { name = "pydantic", specifier = ">=2.13.0" }, + { name = "pydantic-settings", specifier = ">=2.14.0" }, + { name = "unidiff", specifier = ">=0.7.0" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.28.0" }, + { name = "mypy", specifier = ">=2.1.0" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.15.0" }, +] + +[[package]] +name = "inalpha-paper" +version = "0.2.0" +source = { editable = "../paper" } +dependencies = [ + { name = "alembic" }, + { name = "argon2-cffi" }, + { name = "backtester-mcp" }, + { name = "exchange-calendars" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "inalpha-shared" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.13" }, + { name = "argon2-cffi", specifier = ">=23" }, + { name = "backtester-mcp", specifier = ">=0.1.0" }, + { name = "exchange-calendars", specifier = ">=4.5" }, + { name = "fastapi", specifier = ">=0.136.0" }, + { name = "httpx", specifier = ">=0.28.0" }, + { name = "inalpha-shared", editable = "../_shared" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.47.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.1.0" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "respx", specifier = ">=0.23.0" }, + { name = "ruff", specifier = ">=0.15.0" }, +] + +[[package]] +name = "inalpha-shared" +version = "0.2.0" +source = { editable = "../_shared" } +dependencies = [ + { name = "fastapi" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "structlog" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.0" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.0" }, + { name = "pydantic", specifier = ">=2.13.0" }, + { name = "pydantic-settings", specifier = ">=2.14.0" }, + { name = "pyjwt", specifier = ">=2.13.0" }, + { name = "python-dotenv", specifier = ">=1.2.0" }, + { name = "structlog", specifier = ">=25.5.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.28.0" }, + { name = "mypy", specifier = ">=2.1.0" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.0" }, +] + +[[package]] +name = "inalpha-shared-llm" +version = "0.1.0" +source = { editable = "../_shared/llm" } +dependencies = [ + { name = "anthropic" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", specifier = ">=0.60.0" }, + { name = "pydantic", specifier = ">=2.13.0" }, + { name = "pydantic-settings", specifier = ">=2.14.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.1.0" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "respx", specifier = ">=0.23.0" }, + { name = "ruff", specifier = ">=0.15.0" }, +] + +[[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 = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "korean-lunar-calendar" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/9f/6160e95f3934026194dc7a0b5c78fe5c6e74761830ddea3e9cfc38e3f983/korean_lunar_calendar-0.4.0.tar.gz", hash = "sha256:be56f27bc0594fdbbdf7bbe00f504a9f929a31e311bd7d9bb93561b645afade7", size = 13425, upload-time = "2026-06-15T14:19:57.163Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/88/f56033b09fdca5c8c5e5fc2f3513950a05cd62caa9bd8e6d2750a887c86e/korean_lunar_calendar-0.4.0-py3-none-any.whl", hash = "sha256:c042e20de0bb702add6bec8d0f6da1ea8d3b170838e63846f70420cf341fe4e7", size = 11323, upload-time = "2026-06-15T14:19:56.089Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/27/72ae94ea5c8f7349ec1c229d4cd058feb799cbd0833ad6d1b47c919b37b7/llvmlite-0.49.0.tar.gz", hash = "sha256:00f16db782f4a13c78c5804aedc434e46794a77e89999a168f9401106270e50a", size = 194467, upload-time = "2026-08-11T16:26:00.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/ae/3f699ebe3590e15e023a6372dd147526fd8ec398aacf9ceb844e854964a8/llvmlite-0.49.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b541c8fac3450db7574d1f53cf9dff83f285bfed9d69bf81fe71fc2a7d4f97fe", size = 40479231, upload-time = "2026-08-11T16:23:56.773Z" }, + { url = "https://files.pythonhosted.org/packages/be/3c/e97f69c62a2d972066d9a2612ce1f3de313035ac897a5b9f787cad8b55f7/llvmlite-0.49.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6acba646d88abbc87d5c113a3d62c1fbf8b8fee11c6493f516803e30f21ae870", size = 59890658, upload-time = "2026-08-11T16:24:05.451Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/e942ee08605fc0526ff3854260c384d8315a5830e16c4c2a5aebc14dc9bf/llvmlite-0.49.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec8ad805e7515cb8440a690eb3cef4d34acb29eef80b705ec4e1c1ad3c43c68", size = 58344481, upload-time = "2026-08-11T16:24:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/49/2a44871cac6b5a2fd4aabd68cfdaf6de9a5c7edb36dee5d47b77bda4b50f/llvmlite-0.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a9c9e3af4e214acfefa4f73ebe7bc3fb35854a62b654edb3953f5ae33c08ba3", size = 41865543, upload-time = "2026-08-11T16:24:20.41Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/0b536a3c59f2636d9dd51dda832b6c1d0ffec37608429dedf128664918f1/llvmlite-0.49.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:039fa4054a06f537fb39248d4472284ca96be311a142ec09e69f95630ab469cc", size = 40479230, upload-time = "2026-08-11T16:24:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/ca8ba47b057b793099784475499771780ec46839f2782f753a7079d23520/llvmlite-0.49.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddc7aecd4f56397ed6e8f120ec5dcd5a1a8f0e6032ca4af413462792d4dca2e3", size = 59890659, upload-time = "2026-08-11T16:24:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/9526dfdd33a923f33e29a18b8f9801ee7ee4b7397e88d28192c1024c4a75/llvmlite-0.49.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dee64784201b64c13a8df62c48a4f4218858faaa65889866bb29bdc243c038", size = 58344482, upload-time = "2026-08-11T16:24:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/9f5afcf6476b228d6b170408f377a0c4f91477fc1fc91f8141088b45bf46/llvmlite-0.49.0-cp313-cp313-win_amd64.whl", hash = "sha256:a1b414dc6b164738ec39dd8987cea73829057b7dd92fc6d91b52838385fc1dd2", size = 41865544, upload-time = "2026-08-11T16:24:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/16599b8c9f21802448059482eab48a9e74086dc56b901a677ba355565e64/llvmlite-0.49.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:80a84683d04516bb51da1bbeebddaf2c2f558809c93078a8f91807909ae331f8", size = 40479230, upload-time = "2026-08-11T16:25:01.513Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/0b23849141a4c4e7091fcd158ebb45195896974bebca3e58fee7cad4b4f4/llvmlite-0.49.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4281a0171d66d2098adce4ba706b8c550b1b10718650f682d64cde16e84e4de5", size = 59890659, upload-time = "2026-08-11T16:25:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/c4/92/628692b74b31e27af9ba7e8ba651941ee4956959d5478123c453f59aad4a/llvmlite-0.49.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b095f15fb12c4d90495df5b1a3772b4732cc408398b204a787dbedd370e09c69", size = 58344479, upload-time = "2026-08-11T16:25:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/96/8a/412fc273521b02cbfe0b5f8ad56cc696385f6eaeecdb9e9ae6a90111d98d/llvmlite-0.49.0-cp314-cp314-win_amd64.whl", hash = "sha256:294e2f0b70aef8f92d0ae7b203e2609f08beb39437eee73de59a21669331aae9", size = 42986588, upload-time = "2026-08-11T16:25:22.534Z" }, + { url = "https://files.pythonhosted.org/packages/fc/15/f47cf45c00c8b165ac3d268502dcb21d900e86f27fd338268a66ce922ab0/llvmlite-0.49.0-cp314-cp314-win_arm64.whl", hash = "sha256:95d1071023ed858b79f6971954fd7cc1f5dbcbab987718a4ccbe1411e47d0b81", size = 37441881, upload-time = "2026-08-11T16:25:28.324Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2e/eafd488766d1c02413cba24f7b22acb9b3ccdfd8407e98d30eb16bac4e2a/llvmlite-0.49.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:f3f2ff0aeb17d34fcce9f79b99baac441cfd3efa41b83e233ca4530a72381f72", size = 40479230, upload-time = "2026-08-11T16:25:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/98/07/a2c4f04e2111ccc274b4d5e3331398a9dcf6d6e5e55d6444b1ad9d6381cf/llvmlite-0.49.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5555ea1d63928481cbf7fcb1d67452b216c7e5b393a4eb7aa1401e67f2a4fc4", size = 59890658, upload-time = "2026-08-11T16:25:43.294Z" }, + { url = "https://files.pythonhosted.org/packages/80/f9/7b7b50f80b4585bcd78675ff3110c256877b11df32a8cde284f851762f57/llvmlite-0.49.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32adb84fdaae28aeb86fdb6253084ee707ee157289a2e98fe3caf48a62bee82", size = 58344482, upload-time = "2026-08-11T16:25:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/32d68bfbf1d0c36888530ef6fd72864861af23dc546302b41033471a8c3a/llvmlite-0.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:be637e465010bc9c50f070468f7f1cf5385e92fee364d192dd5e6cea790ecba9", size = 42986602, upload-time = "2026-08-11T16:25:57.69Z" }, +] + +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numba" +version = "0.67.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/915cddba90010348ed0444451132fdde9b000bcbaff1582029b5bf115d11/numba-0.67.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6004d8d5f28d4028687fb2d972d629295b13685943bd2ed5cd8810c3b848e219", size = 2745050, upload-time = "2026-08-11T23:03:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/bb/38/926757caaac18a66f057d7544a63620bf360a07d281c9f7ecadd2aa83963/numba-0.67.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f63d43db06b4756424d6d2484737c902e0ae944a0eec3e8b0b4de2c695b15caa", size = 3884596, upload-time = "2026-08-11T23:03:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6d/58291dc58da39d98b32db7f044729f6d8d4920cd9622fbab3179b54ff4c4/numba-0.67.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76d3335aaeffb9dc88309420890e73497a00be08a7530441bc2b58ffe025bfa5", size = 3585290, upload-time = "2026-08-11T23:03:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/ab21828b4056afed71f9ecb40f4de26c2c19de731cc001961aca74b79464/numba-0.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:50e2b72406c18cda5dd7431b0082cb85ea94e06c64c33607248fc8bef92cfb81", size = 2815645, upload-time = "2026-08-11T23:03:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/bd9fe772f6c84597b76cac229b3f2890f01a2c64fd70e48ceaae10dd65cb/numba-0.67.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:77e1c7173fee57a0d84e006c7e70346689d6cb3e7db503489bae58646b4eff7b", size = 2744872, upload-time = "2026-08-11T23:03:33.649Z" }, + { url = "https://files.pythonhosted.org/packages/a1/1c/c05609739cc41116d36e30cb2b41fb00f126bb52e1b0bac907051ad8a35d/numba-0.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4953387c77864b596d8296e2cfbdef82b0eea4166ab4864b05d226c51143e0", size = 3892004, upload-time = "2026-08-11T23:03:35.797Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/a5276ad4178250403e0e2251f3e1f8ac18feac779b0474a8bcb08558490d/numba-0.67.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f6e0f5cb6c545e158b6ef0496c01b6d6958a7ccc6634a1576a94bbbab29ff2", size = 3591878, upload-time = "2026-08-11T23:03:37.845Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/d48f0ba7442516ceb5a1585f0c81d3aa531bc96bfcabcd9f8f925768c426/numba-0.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:b68ad5125fe245339cc8dcc036081fc1ea482c5063387b9612a76ccd83dc91cd", size = 2815504, upload-time = "2026-08-11T23:03:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/345b1e4774a08247aafcfdb93d4e8d24a3646366cbe72de33053fc0de1b5/numba-0.67.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f99f880ff25f418a67f9a1d00d0ddfbc63430f627b523e515085a592a7567f4b", size = 2745088, upload-time = "2026-08-11T23:03:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/e614ba2bc0f005ed0f37a6413f08fe705210297ddb9a37a475a8b9fdab61/numba-0.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269245a675abdd3e2c35ec6bb2f250355effa9032514d8f2354f0d2d10854bd", size = 3861040, upload-time = "2026-08-11T23:03:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/30c42a1dbc4176cf355e8e8be61803732c55597b1332925fe233912a43d9/numba-0.67.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f074a8e23db78490f11a3930c940be758316c10ac5985be83d2f298dc080acf7", size = 3561811, upload-time = "2026-08-11T23:03:46.037Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/21bd16f770476e394c5e5f504935817967442a71251d6b86c244a2767980/numba-0.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4d576e62bf2c9370f61312b51573c4bb1f3fe96798bbab56730847a368a316c4", size = 2817421, upload-time = "2026-08-11T23:03:47.922Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/bb41b0e59b9ff52c94a2f01db24f6437df058caebb377b5f372fc343a6a2/numba-0.67.0-cp314-cp314-win_arm64.whl", hash = "sha256:7930748ce8355d2a5a28602abab056a61fdc676d17377f27d17993905428171f", size = 2788885, upload-time = "2026-08-11T23:03:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/aa07151fbd0f4283f78de437cc196f9084789be89a2b4de3fdc2f6a4b414/numba-0.67.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:4a2ed006635bbd0fe45681ed49f3b4f4bad1abf0c233bcc5842c9e3a34cabd61", size = 2748150, upload-time = "2026-08-11T23:03:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/74/62/b8174ca95a4cc1a7ba1520767734e016991545590b8fbde521b681701a9f/numba-0.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa5f002f665bec321b950dacaa26ee009e1d720f6ac9d9856eed5efe1caa03a6", size = 3896986, upload-time = "2026-08-11T23:03:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/3a7b6dbf81e01a48958b45ad2239edbc64707522ab17f11f9f18c44bf6d1/numba-0.67.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83ab968b0e0fa744eba03351282dd8000796e6ec8e4518f47bd3ed86c0a20c7b", size = 3614644, upload-time = "2026-08-11T23:03:55.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5b/248f5681c121ca853a9f4e39d342a3e01b8a0261b0275853eb3d0d56aa20/numba-0.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00c964a5b94d3ae82d83ac162cd610755875b98dadb779fdde06e6bfcdbca47e", size = 2822870, upload-time = "2026-08-11T23:03:58.097Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "optuna" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/05f5e3f662cc96a4c478fc3446b8ed6359825a2b504ecb614a9ac84e4a4d/optuna-4.9.0.tar.gz", hash = "sha256:b322e5cbdf1655fb84c37646c4a7a1f391de1b47806bbe222e015825d0a82b87", size = 485834, upload-time = "2026-06-01T06:23:30.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/f3/e5fcd5d9b15771ed6dc10e3a7eeddc672e418f4f4c4653d216cc1d857e2d/optuna-4.9.0-py3-none-any.whl", hash = "sha256:f52f3be6148654850c92a5860d398fd88ec6b2c84ab68d9c3d07dcff02e7afee", size = 425553, upload-time = "2026-06-01T06:23:28.804Z" }, +] + +[[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 = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[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 = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size = 50102437, upload-time = "2026-08-10T12:38:22.487Z" }, + { url = "https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size = 49942424, upload-time = "2026-08-10T12:38:28.755Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size = 53144206, upload-time = "2026-08-10T12:38:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size = 27953934, upload-time = "2026-08-10T12:38:39.808Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size = 35855328, upload-time = "2026-08-10T12:38:45.489Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size = 37622415, upload-time = "2026-08-10T12:38:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size = 46813813, upload-time = "2026-08-10T12:38:57.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size = 50104452, upload-time = "2026-08-10T12:39:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "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.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[[package]] +name = "pyluach" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" }, +] + +[[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 = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "structlog" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[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 = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "unidiff" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/48/6ebfbda867e1a07bab3bbffe820e980bff8262c97ff77d1496a4fa15e711/unidiff-1.0.0.tar.gz", hash = "sha256:5e5d5cfab2dc98be819b74747ab7d9f5af8695369ec8710b93f9ab0f0ae6a449", size = 29365, upload-time = "2026-07-25T19:13:59.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/860142913b2fee25c78b3af733054e248c488bd83cf6cfb97969e98e3bcf/unidiff-1.0.0-py3-none-any.whl", hash = "sha256:2e1fb4eebe2354a26a1f3d51efe2e5d504cae5764b98ed8bdbb4e7a000baff28", size = 18279, upload-time = "2026-07-25T19:13:58.797Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/53/be79eff13cc289570b4c6875fa4641a91a1dc51ece7f6213f364b0a58c4c/uvicorn-0.52.2.tar.gz", hash = "sha256:4294500b9c8f7a3ef3e975d9e4be08c3eb76441af449a9e6e10146c6a182ffec", size = 100685, upload-time = "2026-08-13T07:03:57.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/08/0d834cf3e61e476c9f422e856a9e37d12d708d7ada942a2774b1f42aee6c/uvicorn-0.52.2-py3-none-any.whl", hash = "sha256:0b916d247cf5500b320638ea11abf0fed7f348e8e1c7fde53a0e6b6319803512", size = 79912, upload-time = "2026-08-13T07:03:55.658Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/96/e01084f83a64bcb3a27994bd0cb0db68ff29d9c6707fae37ec19b18ba990/websockets-17.0.1.tar.gz", hash = "sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc", size = 183298, upload-time = "2026-07-31T11:31:27.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ff/6199a52d864215750af8668d84b0274775011a90052081f5a9495807a92b/websockets-17.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10f461191125c63902ea7394ae9e752b1b5785641850c1d365bb30b0f88bc53f", size = 212603, upload-time = "2026-07-31T11:29:30.771Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/439a962bcada88dcf586da77a1b2385f91e2d2910e9359540934c827156b/websockets-17.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cffc84ddec6da7f447677266fee2a3c40ecc78172f00752aa1150b8a8d65df1d", size = 210286, upload-time = "2026-07-31T11:29:32.157Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/123660edc759c225626b3b91952c7625f85c77a8362acbc35a4623120f7d/websockets-17.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c23e532c8a2325a1e7486de8763a60dc43e83f01bcaeca07e3ba79652c156db1", size = 210549, upload-time = "2026-07-31T11:29:33.388Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2f/2940e57080cf56f28190287516400126d5a76b52b9a61dc10ba6f6400dbe/websockets-17.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c09e097d0e46e3c289bedab9a475ae344b70c30ff5646e46af22b4e6fdc97b21", size = 219874, upload-time = "2026-07-31T11:29:34.608Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/9ec976c16d63cc51c28dfec74b66854048c0b8b6579946e902f91b69e8bf/websockets-17.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f47b0815af3948ec6a440b3afa02f05b18cc0939549e91b5c677b5d9c2c8472a", size = 220150, upload-time = "2026-07-31T11:29:35.831Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a4/850c699a16bbc451723856360c59bd997bec075e637154f3fa96e80d5760/websockets-17.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8848c207049ad49d318e5f64a3d4d7bb189f8328d0d98e65647788f2a085785c", size = 221389, upload-time = "2026-07-31T11:29:37.189Z" }, + { url = "https://files.pythonhosted.org/packages/47/e1/f60a891c1a4b3420d5052333a84eb7241e1fb4a71866dec1562f5fa30027/websockets-17.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2604de7228506b13a44a256a9d223943340c0e725af5d367dc068e192b027761", size = 224169, upload-time = "2026-07-31T11:29:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/26/fb/e2a893be6fae4fddfe50ddc3035a331d3f381103d5467b7900026bdb3a64/websockets-17.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07abc3bd196a48af476a82fd47f3f79a6a3f70937a9f930cef703cfa0c9d83b6", size = 222025, upload-time = "2026-07-31T11:29:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/d9/72/e3144b2d79276fab9798ed7d4aea2f0847434f186800b6f56a1eddcb3114/websockets-17.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:769ce7e2acfd9a89f2bed3a9c0da229459516bbc00bd4c9e2ca492c613ae4861", size = 220779, upload-time = "2026-07-31T11:29:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/69c02174fbcf1c40c6adc45d3c316a401558392fe7bab8969ef8c46f1689/websockets-17.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07d78a509c3333f5908c83d7f78144ea68a6c9ec28110f5c54d81d8fcdc262c4", size = 218053, upload-time = "2026-07-31T11:29:42.322Z" }, + { url = "https://files.pythonhosted.org/packages/b3/09/7574778b095b99cfa0856583462f56568df784f9b41485145169b2ec9c64/websockets-17.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ffad64ce7ad3703d652a3fd9af26238377d24ce52c6ad8ff35d26d82f61f493f", size = 220825, upload-time = "2026-07-31T11:29:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e6/f46571f38765dbc4cbc0d0b47de8db65768006dbbd4340e6f5f51bc1d895/websockets-17.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e95e321d0d763f2b6633512605f6112ebd70d5746f3ce05c941909d4a25233f2", size = 219427, upload-time = "2026-07-31T11:29:44.731Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/6ff1fee057bd7e9dd5237fc064a749615378d003aa045b5bfc2d12b2f4f7/websockets-17.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd526c8228e759c1006c4b7c9ac71dc4e925ced1a6a6a5a8e94643709738f63e", size = 220198, upload-time = "2026-07-31T11:29:45.997Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/d41847227a44b9ad87c3d5a9fddbfad8b7c4d6032878d8460d9d37c2d44f/websockets-17.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd3369e42c0246afaf9d669cfc19797e3a49e8c0a639544459c57597108b966", size = 221304, upload-time = "2026-07-31T11:29:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/63/30/21a7e326c6ad2eb526cd5b816383d59cdeb28b8805b65a543c3cfbd8e8ce/websockets-17.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b580794e926cab7ff42ee4371ef14e0b22cb2bb722a607f77769136468f49a3f", size = 218858, upload-time = "2026-07-31T11:29:48.587Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/55b0331cd5bec9ce29748f79edc00075805450a47011d0c8e3b1c61dbf04/websockets-17.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5033ffe6804dd53afafa7d08e8c3eef2d2431f34d58ca30507a8442dd04a033a", size = 219840, upload-time = "2026-07-31T11:29:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/2e8bc06e546f49b32a58a2bc2957902d1809ecc37552d3d7ccd6639a126e/websockets-17.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6db9e5bf3649ab506c6ae8a3ac85a00fb1ae3816d75962771b2df8adbc5d40d2", size = 220116, upload-time = "2026-07-31T11:29:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ad/4bac01fa41aca54307157b9c9f68b066a6bb51fb18716ba618078a67b283/websockets-17.0.1-cp312-cp312-win32.whl", hash = "sha256:bc0bca48ba24c6c866847fd20478a51dd547fa0ad258dab9615c414ec534bbc0", size = 213050, upload-time = "2026-07-31T11:29:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/c3a78cccc74a554780e9e76e323d5cde891048627025f0f82623e22dc3df/websockets-17.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2b3f3020171202b135ca078e20434977c6b2b02af647130d6980c9e39b9462e3", size = 213348, upload-time = "2026-07-31T11:29:53.891Z" }, + { url = "https://files.pythonhosted.org/packages/7b/25/e1b8824bd632c8a5a62d504b61e9e35e470b67e4be0206f5c28f90c7f86d/websockets-17.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:41d6aa06b5ab832aee72fedf47a149535b121ac900b6bb4d3fe14712afac9a79", size = 213276, upload-time = "2026-07-31T11:29:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/79c577bc2f874ee22f6f5ccdab97ba9ce6b96806be3fcc3a6d8490f88a21/websockets-17.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:55b12e47dcee83673a40d07686cfb6f9d6dfc285976ade9463f61d2bef3fad22", size = 212593, upload-time = "2026-07-31T11:29:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/e1cfaf419bb3b2fcfd6792a846f1d936293132b0b9a56530ced016c83c7b/websockets-17.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c118a6b0e25bfc9a6802075d748fa6321714ffbdf3c88d29d9a0e3c7386c75", size = 210280, upload-time = "2026-07-31T11:29:57.768Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/cc994494bf7d97e41833f6ff55c24f535e4d527a10370b9631737e9c2f00/websockets-17.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:734d20364dc2cfe03674883cafcf580b6e431c5ce42b476312b9285310230cf9", size = 210538, upload-time = "2026-07-31T11:29:59.021Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/fbf2d132f63ba3e67f675bccf333469786a24e0418969ce1d8e6ff9e6f02/websockets-17.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9493314a99e599163c854fb5900ad7f7ea38c5cb9d9103aa30b3c6b8181c01fa", size = 219925, upload-time = "2026-07-31T11:30:00.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/64eee3d25a47fe744a9490e0627cc373dca096755db740f91c28bd61cd35/websockets-17.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18ded646ce98cdd3c0235825b3252f1df55765ba49b616bb10282f758667b4d0", size = 220206, upload-time = "2026-07-31T11:30:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/15/56/10ed4bc4dd75f204e3c62bd4898e44a8742a27773c80b188cfa7888aad2d/websockets-17.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1bec5d6a19f5fbe87e4940739cfc65e7bb53d8b353e1029b8037a1653b321bc", size = 221445, upload-time = "2026-07-31T11:30:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/bf/be/bb14328614c068ab09569962fbf218fc00413ce3febc6d2684c764b6f37e/websockets-17.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:872273e629ca7e3d35f16a2dc6ede84e1d5c831e616b8277de6e4f83114e7c58", size = 222887, upload-time = "2026-07-31T11:30:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/32/1b/4cb0eec2fee310007104687493175af190019f705940c864f9c523fe9f6f/websockets-17.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1df81d174c1561292de9e40b141cafc04f69077272f6c352afe1d743e20810df", size = 222072, upload-time = "2026-07-31T11:30:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/14e6391de777ddb39c439c450deb551406d445e25a5877d6fa25c49d4544/websockets-17.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:759adeb5b0c5775b563254ec63b5b79089fc0045b479143a0b1b8c0ebaae1253", size = 220826, upload-time = "2026-07-31T11:30:06.53Z" }, + { url = "https://files.pythonhosted.org/packages/cb/57/96e94e384442247bbed5d3ab67381c7257355c2d66b62c3ad33a17f5d385/websockets-17.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d99db29b5444e3982f1ce2ba8a833508ad44b2f1fbd0bd99e81d825c0b461", size = 218107, upload-time = "2026-07-31T11:30:07.766Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/9c9dedd14c3919435df9b35cdee7111268c751252b87652f3a6a4f56e760/websockets-17.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:02ed63bf26dda9fa27df730a41f6664586c4ee05972c8fb667ce1725b3fd13d3", size = 220889, upload-time = "2026-07-31T11:30:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/ca73c2ac82c00f50c529784bacb323e42da4816333211bc1543d90c9cf11/websockets-17.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:eab6de8a98b9a7772cf686d00b4de439fc7efb8ab05ae106ef227291d06f87c5", size = 219486, upload-time = "2026-07-31T11:30:10.289Z" }, + { url = "https://files.pythonhosted.org/packages/5b/da/fb37ac09dcd7c69dd73bac979ed393df35f78a3c232e293d1ff3bd586d24/websockets-17.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2a855b6dfe21c4d3420be265ae031829ba8ba0be0ea350d9f7c3ef30ae63ebe2", size = 220258, upload-time = "2026-07-31T11:30:11.605Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9693f191d968a93f37326a17301a101d49580889c688f466699f89ecdee1/websockets-17.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7002d5f9e1c3ddd991cdfdbfee18cc8c8b196b2445022892badacd6cb338bbbc", size = 221358, upload-time = "2026-07-31T11:30:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/cf/29/ad0d85c01db5dcf22898d51648bd2c25af0dd0a4a41c550b11acddeeeba7/websockets-17.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c395bda8e7d8f51a02e80261fb57127979e5c472675d9a96b2860619ad47da48", size = 218921, upload-time = "2026-07-31T11:30:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/18/3b/bf8e855e495dcca63f2b8aa019cf2ada3160e1fa66d833c7417f3b1f7f38/websockets-17.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aadc298969ad229d8e3029fc5cc751fdad286696230f9cf014e90ff9cd8e6ea0", size = 219871, upload-time = "2026-07-31T11:30:15.358Z" }, + { url = "https://files.pythonhosted.org/packages/3b/db/c7abd6639a93a40279cd1ddc57e09e1c4f8381c4cfccdb775aa5aac9770a/websockets-17.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f11a398d8170b7ac5000baf7f258dcda579ef3ea744e0cc6a165e0dfbc0d3198", size = 220154, upload-time = "2026-07-31T11:30:16.96Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/25a9f8f2e5a6ef34e911d2f55d9f756bdeb92b4c28cfb77b8430bbc73cb1/websockets-17.0.1-cp313-cp313-win32.whl", hash = "sha256:846a4a8b0833e3cad57523d9e3bd50ec8ea05ab9d06c582f82a1340ba096af5f", size = 213038, upload-time = "2026-07-31T11:30:18.434Z" }, + { url = "https://files.pythonhosted.org/packages/81/2f/ea1380f72bb11b64fc5bc7ae0d42de5bbf3e6dc13b965706b2a1d4e17cdf/websockets-17.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:409d93efcaa14f7a99592c5baaef5ec6ca94fba0f5aec1a86f693977c69c9c1c", size = 213348, upload-time = "2026-07-31T11:30:19.693Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c7/b956ed9151c3c74530ebc62d716fbfdbde7507a6acc6423a64f9ecfb6b8a/websockets-17.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:90246fa9e6cb192a778ce6ce024057ec54317a894db7899c922dcdc1f4cbf6a5", size = 213282, upload-time = "2026-07-31T11:30:21.045Z" }, + { url = "https://files.pythonhosted.org/packages/98/dc/cadab608924ac605647031472fb1f8792d7d4ea07565ba1899ec42028e0d/websockets-17.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e", size = 212640, upload-time = "2026-07-31T11:30:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/7a/b034d13ca181211bbd58bb50835cb196a7784cd505b5a2079d4d03374f9f/websockets-17.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c", size = 210332, upload-time = "2026-07-31T11:30:23.608Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4d/943ede39b53744768edf1ed84a3f9401527388228a3d6c1249c02c3d6bd7/websockets-17.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163", size = 210546, upload-time = "2026-07-31T11:30:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/88dc159d2ae66743c669443246243f28d873b0c5e58271b8cc1ca0440334/websockets-17.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0", size = 219928, upload-time = "2026-07-31T11:30:26.221Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f2/ff27eaefa15851a5cf7f004ab827a022bf2d6632cb520f89cb100db7e84b/websockets-17.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c", size = 220279, upload-time = "2026-07-31T11:30:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/a5166149f363d2449c1cb2dde6486a245521979509d53b87a09f3e79662b/websockets-17.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63", size = 221525, upload-time = "2026-07-31T11:30:28.872Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/f46931269b3ff3bde65d27c65ddb22f9bb8ce92ac2c6c4df0910128f6219/websockets-17.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe", size = 222897, upload-time = "2026-07-31T11:30:30.164Z" }, + { url = "https://files.pythonhosted.org/packages/42/f4/deccf3439f35df953ec35e13fe07986821c5f1ab5785d69614283bdb9034/websockets-17.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594", size = 222129, upload-time = "2026-07-31T11:30:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/35/a5/e1b57a59da92ade37fd021567a17b518ea8267b28e5530075844cdb525fe/websockets-17.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283", size = 220875, upload-time = "2026-07-31T11:30:32.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/e7d0889c790a854156de424575fd67af79ddbaed9ff3157ae863dfd1c1dc/websockets-17.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e", size = 218160, upload-time = "2026-07-31T11:30:34.512Z" }, + { url = "https://files.pythonhosted.org/packages/09/2e/43db785d6ed9ae7594fae7b62bbc9cb4dfee2b015e06a1005f1e5ce283b6/websockets-17.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed", size = 220951, upload-time = "2026-07-31T11:30:35.806Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/4ac3cab3d5e8a830657a822f64a8910e3803229c6783e59c3fd9a3487427/websockets-17.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78", size = 219460, upload-time = "2026-07-31T11:30:37.273Z" }, + { url = "https://files.pythonhosted.org/packages/03/0e/c3a4020673ffc17c82cf1a467835038a196a555d3b4f2a50f0f063cf8ccc/websockets-17.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f", size = 220248, upload-time = "2026-07-31T11:30:38.527Z" }, + { url = "https://files.pythonhosted.org/packages/7d/87/e47a6a278cc1dfade38444c893ce18322943c25d4b780a74450d9d164be1/websockets-17.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7", size = 221421, upload-time = "2026-07-31T11:30:39.879Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/473d5fc4e3836e375b0233c6ef26777e6e5e3f7bfc84ccd524eae4090ed5/websockets-17.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685", size = 218975, upload-time = "2026-07-31T11:30:41.192Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b9/819ec2dcdf69031d7e9cab11247f3a6ff9bbc8c7c53ada1dbcb9055b227b/websockets-17.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51", size = 219925, upload-time = "2026-07-31T11:30:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/85/b9/6c0da301f6118502e079cf92f4e864adf28e56b3f8c0f6085076ced7b876/websockets-17.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b", size = 220218, upload-time = "2026-07-31T11:30:44.04Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/cba32dc9dd856565263d6272f594255bd0e2781deb8cd982c026a54760ad/websockets-17.0.1-cp314-cp314-win32.whl", hash = "sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b", size = 212626, upload-time = "2026-07-31T11:30:45.579Z" }, + { url = "https://files.pythonhosted.org/packages/fa/95/91cdd8c192287d7ea741f37cf7d64fdc1a14410f06f73805e428a1a590af/websockets-17.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add", size = 212969, upload-time = "2026-07-31T11:30:46.983Z" }, + { url = "https://files.pythonhosted.org/packages/8e/fd/8c98a1e431960661c5769ab1a4dd66494e87ab02d791cc79e51e0d9a289f/websockets-17.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891", size = 212850, upload-time = "2026-07-31T11:30:48.304Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/142f5186ee7dc3beee0426b998a79e223e067b7689afcaa95890d64aa800/websockets-17.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf", size = 212967, upload-time = "2026-07-31T11:30:49.688Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/4b03ed316c9dee180365286c298219809ff247be39beeaaf9958b21167ab/websockets-17.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed", size = 210504, upload-time = "2026-07-31T11:30:50.987Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d8/ad2b3e8f867e1e8cac3077e2f33ffb60b71bd763d6cfc71bd916f113c3bf/websockets-17.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce", size = 210702, upload-time = "2026-07-31T11:30:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/7a77a82ce9d6f831c07b176da3942f7e71acd0f115f3ecdb1d00a040eb01/websockets-17.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38", size = 220290, upload-time = "2026-07-31T11:30:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/f5/af/43c3e3c3ea7ba4693c2181743f3221957df28bededadcbd9fc8a0661bde0/websockets-17.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d", size = 220573, upload-time = "2026-07-31T11:30:54.966Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bd/ed48eca15725743ee7e2dc172e15c61de29e85ec98dace7b14257f366836/websockets-17.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed", size = 221747, upload-time = "2026-07-31T11:30:56.762Z" }, + { url = "https://files.pythonhosted.org/packages/99/50/838deb7937a8225c4925dd4a977eafea473fabf444178a99de0bc7e92bb0/websockets-17.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6", size = 223891, upload-time = "2026-07-31T11:30:58.127Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e9/657fb70c6eb6bcd01adfa5d2b06496e9911e1c1a8813d353b8c00f7591cd/websockets-17.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605", size = 222317, upload-time = "2026-07-31T11:30:59.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/4c/82cb722afa5428fed981331210c4c07600570db01bb1620579f655b5adaf/websockets-17.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442", size = 221047, upload-time = "2026-07-31T11:31:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/bd6d67d6bc65f0de0cb50de55dab42f256a9876a351c4736522eb168fda0/websockets-17.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2", size = 218626, upload-time = "2026-07-31T11:31:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/6a372c8553976f0d8f97f5115826ed47e34b3be6b8bf0d0249af249a7416/websockets-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87", size = 221299, upload-time = "2026-07-31T11:31:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/f966e8472337974f74d788b3ef6c6f3b8b9a5f201efd16a843c91d269fa5/websockets-17.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6", size = 219789, upload-time = "2026-07-31T11:31:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/404e83a6cc7b0efcac810b7041bffd72ff76900e6fd0aa45a26c92fb2ffe/websockets-17.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a", size = 220678, upload-time = "2026-07-31T11:31:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/8946188c2a68d67251859b589a3634918cf7867bf0b891347a5ecaa43d30/websockets-17.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb", size = 221697, upload-time = "2026-07-31T11:31:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/16/ee73fc2083a2938ac6209f4ec804960496835b20a0068dbcfe8424957c04/websockets-17.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab", size = 219390, upload-time = "2026-07-31T11:31:09.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/d5f88033c69932af6cdaa72da62516ade47c257e3bf69f4c0ba5f40e12a2/websockets-17.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab", size = 220161, upload-time = "2026-07-31T11:31:10.915Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/6183dd2c0370287ecf4afe0bb33aca364208e5e7b0e1a286adcaecc0c78b/websockets-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d", size = 220591, upload-time = "2026-07-31T11:31:12.289Z" }, + { url = "https://files.pythonhosted.org/packages/26/93/70f6516d85b9744f7eac224c4b1b9ef4e84133f80b53be02080cb1c3e663/websockets-17.0.1-cp314-cp314t-win32.whl", hash = "sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10", size = 212755, upload-time = "2026-07-31T11:31:13.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/2c/9d9c1da5a7ea9af307b386d25f64d1dead4729644198d2b92e36db5dfd41/websockets-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833", size = 213094, upload-time = "2026-07-31T11:31:15.367Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, +] diff --git a/services/paper/src/inalpha_paper/api/risk.py b/services/paper/src/inalpha_paper/api/risk.py index 701dda6e..6db727be 100644 --- a/services/paper/src/inalpha_paper/api/risk.py +++ b/services/paper/src/inalpha_paper/api/risk.py @@ -27,6 +27,7 @@ from inalpha_shared.db import DBConn from pydantic import BaseModel, Field +from ..account_id import account_id_from_user from ..execution.risk_rules import ( ClosedTradeRecord, RiskRulesConfig, @@ -163,14 +164,14 @@ async def list_rules( @router.get("/locks", response_model=LocksListResponse) async def list_locks( - _user: Annotated[User, Depends(get_current_user)], + user: Annotated[User, Depends(get_current_user)], conn: DBConn, scope: str | None = None, market: str | None = None, symbol: str | None = None, limit: int = 100, ) -> LocksListResponse: - """列 PostgreSQL `risk_locks` 中 `now` 仍生效的锁。""" + """列 PostgreSQL `risk_locks` 中 `now` 仍生效的锁(按账户隔离)。""" # 与 /locks/history 对称:防 ?limit=999999 直接 LIMIT 全表扫。 bounded = max(1, min(limit, 200)) rows: list[dict[str, Any]] = await locks_store.list_active( @@ -179,6 +180,7 @@ async def list_locks( scope=scope, market=market, symbol=symbol, + account_id=str(account_id_from_user(user)), limit=bounded, ) return LocksListResponse(locks=[LockResponse.model_validate(r) for r in rows]) @@ -186,17 +188,19 @@ async def list_locks( @router.get("/locks/history", response_model=RecentLocksListResponse) async def list_locks_history( - _user: Annotated[User, Depends(get_current_user)], + user: Annotated[User, Depends(get_current_user)], conn: DBConn, limit: int = 50, ) -> RecentLocksListResponse: - """列**最近**风控锁(含已过期 / 已解锁),按 ``locked_at`` DESC。 + """列**最近**风控锁(含已过期 / 已解锁),按 ``locked_at`` DESC(按账户隔离)。 风控锁多为短时效(如 CooldownRule 5min),``/locks`` 只看 active 会让「刚触发过 风控」过期即不可查。本端点给 UI / 审计一个事后复盘的历史视图。 """ bounded = max(1, min(limit, 200)) - rows: list[dict[str, Any]] = await locks_store.list_recent(conn, limit=bounded) + rows: list[dict[str, Any]] = await locks_store.list_recent( + conn, account_id=str(account_id_from_user(user)), limit=bounded + ) return RecentLocksListResponse( locks=[RecentLockResponse.model_validate(r) for r in rows] ) @@ -209,9 +213,13 @@ async def unlock( user: Annotated[User, Depends(get_current_user)], conn: DBConn, ) -> dict[str, bool]: - """人工解锁。`unlocked_by = user.sub`,软删(active=FALSE)。""" + """人工解锁。`unlocked_by = user.sub`,软删(active=FALSE)。仅限本账户的锁。""" ok = await locks_store.manual_unlock( - conn, lock_id, unlocked_by=user.user_id, unlock_reason=body.reason + conn, + lock_id, + unlocked_by=user.user_id, + unlock_reason=body.reason, + account_id=str(account_id_from_user(user)), ) if not ok: raise HTTPException( diff --git a/services/paper/src/inalpha_paper/bar_conversion.py b/services/paper/src/inalpha_paper/bar_conversion.py new file mode 100644 index 00000000..d1368c39 --- /dev/null +++ b/services/paper/src/inalpha_paper/bar_conversion.py @@ -0,0 +1,40 @@ +"""data-service bar 响应到 paper 内核模型的转换。""" +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from .kernel.clock import datetime_to_ns +from .kernel.identifiers import InstrumentId +from .model.data import Bar + + +def bar_from_dict( + data: dict[str, Any], + instrument_id: InstrumentId, + timeframe: str, +) -> Bar: + """把 data-service ``BarResponse`` 转成内核 ``Bar``。""" + raw_ts = data["ts"] + timestamp = ( + datetime.fromisoformat(raw_ts.replace("Z", "+00:00")) + if isinstance(raw_ts, str) + else raw_ts + ) + if timestamp.tzinfo is None: + timestamp = timestamp.replace(tzinfo=UTC) + ts_ns = datetime_to_ns(timestamp) + return Bar( + instrument_id=instrument_id, + timeframe=timeframe, + open=float(data["open"]), + high=float(data["high"]), + low=float(data["low"]), + close=float(data["close"]), + volume=float(data["volume"]), + ts_event=ts_ns, + ts_init=ts_ns, + ) + + +__all__ = ["bar_from_dict"] diff --git a/services/paper/src/inalpha_paper/engine/backtest.py b/services/paper/src/inalpha_paper/engine/backtest.py index 169edb0a..7fe26932 100644 --- a/services/paper/src/inalpha_paper/engine/backtest.py +++ b/services/paper/src/inalpha_paper/engine/backtest.py @@ -60,6 +60,7 @@ def __init__( trading_mode: str = "spot", leverage: int = 1, funding_rate: float = 0.0, + annualization_periods: int | None = None, ) -> None: """初始化。 @@ -102,6 +103,7 @@ def __init__( # funding 序列见 data.fetch_perp_funding_rate(后续把逐根真 rate 喂进来)。 self._trading_mode = trading_mode self._funding_rate = funding_rate + self._annualization_periods = annualization_periods # spot 守门:让 SimulatedExchange 撮合前能 query portfolio cash / position # (ADR-0032 BuyingPowerRule 撮合层兜底实现,旧 BTC -98% bug 同源防御) self.exchange.bind_portfolio(self.portfolio) @@ -216,6 +218,7 @@ def _build_report(self, bars: list[Bar]) -> BacktestReport: period_start=_ts_to_dt(bars[0].ts_event) if bars else None, period_end=_ts_to_dt(bars[-1].ts_event) if bars else None, timeframe=timeframe, + annualization_periods=self._annualization_periods, ) diff --git a/services/paper/src/inalpha_paper/engine/pool.py b/services/paper/src/inalpha_paper/engine/pool.py index c1e8b4c4..714f44f2 100644 --- a/services/paper/src/inalpha_paper/engine/pool.py +++ b/services/paper/src/inalpha_paper/engine/pool.py @@ -90,6 +90,11 @@ def _worker_init(cpu_soft: int, mem_bytes: int) -> None: _set_rlimits(cpu_soft=cpu_soft, mem_bytes=mem_bytes) +def configure_worker_limits(cpu_soft: int, mem_bytes: int) -> None: + """为一次性回测子进程配置资源上限。""" + _worker_init(cpu_soft, mem_bytes) + + def _noop() -> int: """预热任务:让 worker 完成 fork + import inalpha_paper 一次。返进程号给 sanity check。""" return os.getpid() diff --git a/services/paper/src/inalpha_paper/engine/report.py b/services/paper/src/inalpha_paper/engine/report.py index 679114fe..5c068313 100644 --- a/services/paper/src/inalpha_paper/engine/report.py +++ b/services/paper/src/inalpha_paper/engine/report.py @@ -139,6 +139,7 @@ def from_portfolio( period_start: datetime | None, period_end: datetime | None, timeframe: str, + annualization_periods: int | None = None, ) -> BacktestReport: """从 ``Portfolio`` 状态 + 元数据构造完整报告。 @@ -148,7 +149,7 @@ def from_portfolio( equity_curve = portfolio.equity_curve equity_values = [eq for _ts, eq in equity_curve] returns = metrics.bar_returns(equity_values) - ppy = metrics.periods_per_year(timeframe) + ppy = annualization_periods or metrics.periods_per_year(timeframe) final_equity_v = portfolio.equity() final_cash_v = portfolio.cash diff --git a/services/paper/src/inalpha_paper/evaluation_executor.py b/services/paper/src/inalpha_paper/evaluation_executor.py new file mode 100644 index 00000000..f663360c --- /dev/null +++ b/services/paper/src/inalpha_paper/evaluation_executor.py @@ -0,0 +1,123 @@ +"""可取消、可超时的单次回测子进程执行器。""" + +from __future__ import annotations + +import asyncio +import multiprocessing +from multiprocessing.connection import Connection +from typing import Any + +from .engine.pool import configure_worker_limits +from .evaluation_worker import run_engine_worker + + +class WorkerExecutionError(RuntimeError): + """子进程中的受控失败,不携带不可序列化的异常对象。""" + + def __init__(self, message: str, *, code: str, error_type: str) -> None: + super().__init__(message) + self.code = code + self.error_type = error_type + + +def _child_entry( + connection: Connection, + kwargs: dict[str, Any], + cpu_limit_s: int, + mem_bytes: int, +) -> None: + configure_worker_limits(cpu_limit_s, mem_bytes) + try: + connection.send(("ok", run_engine_worker(**kwargs))) + except BaseException as exc: + connection.send( + ( + "error", + { + "code": getattr(exc, "code", "EVALUATION_WORKER_FAILED"), + "error_type": type(exc).__name__, + "message": str(exc)[:1000], + }, + ) + ) + finally: + connection.close() + + +def _cleanup_process(process: multiprocessing.Process) -> None: + """在线程中完成可能阻塞的 terminate/join/kill/close。""" + try: + if process.is_alive(): + process.terminate() + process.join(timeout=1.0) + if process.is_alive(): + process.kill() + process.join(timeout=1.0) + except (AssertionError, ValueError): + pass + finally: + try: + process.close() + except ValueError: + pass + + +class KillableEngineRunner: + """每次评估启动一个 spawn 子进程,取消或超时时强制终止。""" + + def __init__(self, *, timeout_s: float, mem_gb: float) -> None: + self._timeout_s = timeout_s + self._cpu_limit_s = max(1, int(timeout_s)) + self._mem_bytes = int(mem_gb * 1024**3) + self._context = multiprocessing.get_context("spawn") + + async def __call__(self, **kwargs: Any) -> Any: + receive, send = self._context.Pipe(duplex=False) + process = self._context.Process( + target=_child_entry, + args=(send, kwargs, self._cpu_limit_s, self._mem_bytes), + ) + loop = asyncio.get_running_loop() + deadline = loop.time() + self._timeout_s + started = False + try: + await asyncio.wait_for( + asyncio.to_thread(process.start), timeout=max(0.001, deadline - loop.time()) + ) + started = True + send.close() + while not receive.poll(): + if not process.is_alive(): + raise WorkerExecutionError( + f"backtest worker exited without result: {process.exitcode}", + code="EVALUATION_WORKER_EXITED", + error_type="ProcessExit", + ) + if loop.time() >= deadline: + raise TimeoutError(f"backtest worker exceeded {self._timeout_s:.1f}s") + await asyncio.sleep(0.01) + kind, payload = await asyncio.wait_for( + asyncio.to_thread(receive.recv), + timeout=max(0.001, deadline - loop.time()), + ) + if kind == "ok": + return payload + if kind == "error": + raise WorkerExecutionError( + payload["message"], + code=payload["code"], + error_type=payload["error_type"], + ) + raise WorkerExecutionError( + "backtest worker returned an unknown envelope", + code="EVALUATION_WORKER_PROTOCOL_ERROR", + error_type="ProtocolError", + ) + finally: + receive.close() + if not started: + send.close() + await asyncio.shield(asyncio.to_thread(_cleanup_process, process)) + + +__all__ = ["KillableEngineRunner", "WorkerExecutionError"] diff --git a/services/paper/src/inalpha_paper/evaluation_metrics.py b/services/paper/src/inalpha_paper/evaluation_metrics.py new file mode 100644 index 00000000..52bbaab3 --- /dev/null +++ b/services/paper/src/inalpha_paper/evaluation_metrics.py @@ -0,0 +1,114 @@ +"""策略评估共用的 fitness 与 holdout 指标。""" +from __future__ import annotations + +import logging +from typing import Any + +from .engine.metrics import bar_returns, max_drawdown_pct, sharpe_ratio +from .engine.robustness import bootstrap_sharpe_ci +from .schemas import ValidationBlock, ValidationSegment +from .strategy_authoring import FitnessInputs, calmar_from_report, compose_fitness + +logger = logging.getLogger(__name__) + + +def fitness_from_report(report: Any, *, bars_per_year: float) -> float: + """按 paper 的多目标口径计算单次回测 fitness。""" + calmar = calmar_from_report( + total_return_pct=report.total_return_pct, + max_drawdown_pct=report.max_drawdown_pct, + num_bars_processed=report.num_bars_processed, + bars_per_year=bars_per_year, + ) + return compose_fitness( + FitnessInputs( + sharpe=report.sharpe, + calmar=calmar, + max_drawdown_pct=report.max_drawdown_pct, + num_trades=report.num_trades, + num_bars_processed=report.num_bars_processed, + ) + ) + + +def validation_from_report( + report: Any, + *, + split: float, + bars_per_year: float, +) -> ValidationBlock | None: + """把单次回测曲线按时间切为 train/holdout 两段。""" + curve: list[tuple[int, float]] = report.equity_curve + if len(curve) < 10: + return None + split_idx = int(len(curve) * split) + if split_idx < 2 or len(curve) - split_idx < 2: + return None + + cut_ts_ns = curve[split_idx][0] + values = [equity for _ts, equity in curve] + train_values = values[:split_idx] + holdout_values = values[split_idx - 1 :] + fills = list(getattr(report, "fills", []) or []) + train_fills = sum(1 for fill in fills if fill.ts_ns < cut_ts_ns) + holdout_fills = len(fills) - train_fills + + def segment( + segment_values: list[float], + num_trades: int, + *, + bar_count: int | None = None, + ) -> ValidationSegment: + returns = bar_returns(segment_values) + return ValidationSegment( + sharpe=sharpe_ratio(returns, int(bars_per_year)), + total_return_pct=( + (segment_values[-1] / segment_values[0] - 1.0) * 100.0 + if segment_values[0] > 0 + else 0.0 + ), + max_drawdown_pct=max_drawdown_pct(segment_values), + num_trades=num_trades, + num_bars=bar_count if bar_count is not None else len(segment_values), + ) + + train = segment(train_values, train_fills) + holdout = segment( + holdout_values, + holdout_fills, + bar_count=len(holdout_values) - 1, + ) + flags: list[str] = [] + if holdout.num_bars < 30 or holdout.num_trades < 2 or report.num_trades < 5: + flags.append("insufficient_sample") + + decay_ratio: float | None = None + if train.sharpe is None or holdout.sharpe is None: + flags.append("sharpe_undefined") + elif train.sharpe <= 0: + flags.append("train_sharpe_nonpositive") + else: + decay_ratio = holdout.sharpe / train.sharpe + + ci_includes_zero: bool | None = None + holdout_returns = bar_returns(holdout_values) + if len(holdout_returns) >= 30: + try: + ci_includes_zero = bootstrap_sharpe_ci( + holdout_returns, + n_samples=1000, + ).ci_includes_zero + except Exception: + logger.warning("bootstrap_sharpe_ci failed", exc_info=True) + + return ValidationBlock( + split_ratio=split, + train=train, + holdout=holdout, + decay_ratio=decay_ratio, + holdout_sharpe_ci_includes_zero=ci_includes_zero, + flags=flags, + ) + + +__all__ = ["fitness_from_report", "validation_from_report"] diff --git a/services/paper/src/inalpha_paper/evaluation_snapshot.py b/services/paper/src/inalpha_paper/evaluation_snapshot.py new file mode 100644 index 00000000..d3db41a5 --- /dev/null +++ b/services/paper/src/inalpha_paper/evaluation_snapshot.py @@ -0,0 +1,90 @@ +"""可持久化的策略评估快照。""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + +from .schemas import ValidationBlock + + +class EvaluationSnapshot(BaseModel): + """稳定且精简的回测结果 DTO,不携带持仓、成交和完整权益曲线。""" + + model_config = ConfigDict(frozen=True) + + schema_version: Literal["e1.report.v1"] = "e1.report.v1" + fitness: float + annualization_periods: float + initial_cash: float + final_equity: float + total_return_pct: float + num_trades: int + total_fees: float + num_bars: int + period_start: datetime | None + period_end: datetime | None + sharpe: float | None + sortino: float | None + calmar: float | None + max_drawdown_pct: float + win_rate: float | None + annualized_return_pct: float | None + annualized_volatility_pct: float | None + profit_factor: float | None + payoff_ratio: float | None + expectancy: float | None + exposure_pct: float | None + protective_exits: int + blew_up: bool + health_warnings: list[str] = Field(default_factory=list, max_length=20) + sharpe_ci_lower: float | None + sharpe_ci_upper: float | None + sharpe_ci_includes_zero: bool | None + validation: ValidationBlock | None + + @classmethod + def from_report( + cls, + report: Any, + *, + fitness: float, + annualization_periods: float, + validation: ValidationBlock | None, + ) -> EvaluationSnapshot: + """把引擎内部报告转成 JSON-safe 快照。""" + warnings = [str(item)[:500] for item in report.health_warnings[:20]] + return cls( + fitness=fitness, + annualization_periods=annualization_periods, + initial_cash=report.initial_cash, + final_equity=report.final_equity, + total_return_pct=report.total_return_pct, + num_trades=report.num_trades, + total_fees=report.total_fees, + num_bars=report.num_bars_processed, + period_start=report.period_start, + period_end=report.period_end, + sharpe=report.sharpe, + sortino=report.sortino, + calmar=report.calmar, + max_drawdown_pct=report.max_drawdown_pct, + win_rate=report.win_rate, + annualized_return_pct=report.annualized_return_pct, + annualized_volatility_pct=report.annualized_volatility_pct, + profit_factor=report.profit_factor, + payoff_ratio=report.payoff_ratio, + expectancy=report.expectancy, + exposure_pct=report.exposure_pct, + protective_exits=report.protective_exits, + blew_up=report.blew_up, + health_warnings=warnings, + sharpe_ci_lower=report.sharpe_ci_lower, + sharpe_ci_upper=report.sharpe_ci_upper, + sharpe_ci_includes_zero=report.sharpe_ci_includes_zero, + validation=validation, + ) + + +__all__ = ["EvaluationSnapshot"] diff --git a/services/paper/src/inalpha_paper/evaluation_worker.py b/services/paper/src/inalpha_paper/evaluation_worker.py new file mode 100644 index 00000000..bd899b62 --- /dev/null +++ b/services/paper/src/inalpha_paper/evaluation_worker.py @@ -0,0 +1,112 @@ +"""可 pickle 的回测 worker。""" + +from __future__ import annotations + +import inspect +from typing import TYPE_CHECKING, Any + +from inalpha_shared.errors import ValidationError + +from .engine.backtest import BacktestEngine +from .strategies import get_strategy_class +from .strategy_authoring import ( + ContractError, + DynamicLoadError, + audit_strategy_code, + load_strategy_class, + verify_strategy_contract, +) + +if TYPE_CHECKING: + from .engine.report import BacktestReport + from .kernel.identifiers import InstrumentId + from .model.data import Bar + + +def run_engine_worker( + *, + bars: list[Bar], + instrument_id: InstrumentId, + timeframe: str, + strategy_id: str | None, + params: dict[str, Any], + initial_cash: float, + fee_rate: float, + candidate_code: str | None = None, + protective_stop_loss_pct: float | None = None, + protective_take_profit_pct: float | None = None, + protective_trailing_stop_pct: float | None = None, + protective_chandelier_atr_mult: float | None = None, + protective_chandelier_atr_period: int = 22, + trading_mode: str = "spot", + leverage: int = 1, + funding_rate: float = 0.0, + annualization_periods: int | None = None, +) -> BacktestReport: + """实例化 engine 与策略并执行冻结 bars,不做 IO。""" + engine = BacktestEngine( + initial_cash=initial_cash, + fee_rate=fee_rate, + protective_stop_loss_pct=protective_stop_loss_pct, + protective_take_profit_pct=protective_take_profit_pct, + protective_trailing_stop_pct=protective_trailing_stop_pct, + protective_chandelier_atr_mult=protective_chandelier_atr_mult, + protective_chandelier_atr_period=protective_chandelier_atr_period, + trading_mode=trading_mode, + leverage=leverage, + funding_rate=funding_rate, + annualization_periods=annualization_periods, + ) + if candidate_code is not None: + audit = audit_strategy_code(candidate_code) + if not audit.ok: + raise ValidationError( + f"strategy source failed audit: {audit.reason()}", + code="CANDIDATE_REAUDIT_FAILED", + ) + try: + strategy_cls = load_strategy_class(candidate_code) + except DynamicLoadError as exc: + raise ValidationError( + f"strategy source failed to load: {exc}", + code="CANDIDATE_LOAD_FAILED", + ) from exc + try: + verify_strategy_contract(strategy_cls) + except ContractError as exc: + raise ValidationError( + f"strategy source failed contract check: {exc}", + code="CANDIDATE_CONTRACT_FAILED", + ) from exc + strategy_name = f"{strategy_cls.__name__}-{instrument_id.symbol}" + else: + if strategy_id is None: + raise ValidationError( + "internal: neither strategy_id nor candidate_code provided", + code="STRATEGY_MISSING", + ) + strategy_cls = get_strategy_class(strategy_id) + strategy_name = f"{strategy_id}-{instrument_id.symbol}" + + strategy_kwargs: dict[str, Any] = dict(params) + try: + signature = inspect.signature(strategy_cls.__init__) + if "initial_cash" in signature.parameters: + strategy_kwargs.setdefault("initial_cash", initial_cash) + if "position_pct" in signature.parameters: + strategy_kwargs.setdefault("position_pct", 1.0) + except (TypeError, ValueError): + pass + strategy = strategy_cls( # type: ignore[call-arg] + name=strategy_name, + clock=engine.clock, + msgbus=engine.msgbus, + instrument_id=instrument_id, + timeframe=timeframe, + **strategy_kwargs, + ) + engine.add_strategy(strategy) + return engine.run(bars) + + +__all__ = ["run_engine_worker"] diff --git a/services/paper/src/inalpha_paper/execution/risk_guard.py b/services/paper/src/inalpha_paper/execution/risk_guard.py index 23285510..ff639e7d 100644 --- a/services/paper/src/inalpha_paper/execution/risk_guard.py +++ b/services/paper/src/inalpha_paper/execution/risk_guard.py @@ -60,6 +60,7 @@ class RiskGuard: 构造: rules: 已实例化的规则列表(用 ``build_rules`` 从 TOML 配置生成) starting_balance: 账户起始余额(喂给 ``check_global`` 算 drawdown) + account_id: 账户归属 UUID 字符串(锁隔离用);None 表示非账户级(backtest / 测试) `rules` 为空 → ``check`` 直接返 ``None``(pass-through,与 RiskEngine 一致)。 """ @@ -69,9 +70,11 @@ def __init__( *, rules: list[RiskRule], starting_balance: float = 10_000.0, + account_id: str | None = None, ) -> None: self._rules = list(rules) self._starting_balance = starting_balance + self._account_id = account_id @property def rule_names(self) -> list[str]: @@ -116,7 +119,7 @@ async def check( ("symbol", {"symbol": symbol_str}), ): existing = await locks_store.is_locked( - conn, now=now, scope=scope, side=side, **kwargs + conn, now=now, scope=scope, side=side, account_id=self._account_id, **kwargs ) if existing is not None: return _rejection_from_lock_row(existing) @@ -129,7 +132,8 @@ async def check( verdict = rule.check_global(now, side, balance) if verdict is not None: return await _record_and_build_rejection( - conn, verdict, instrument_id=None, now=now + conn, verdict, instrument_id=None, now=now, + account_id=self._account_id, ) for rule in self._rules: @@ -137,7 +141,8 @@ async def check( verdict = rule.check_market(instrument_id, now, side, balance) if verdict is not None: return await _record_and_build_rejection( - conn, verdict, instrument_id=None, now=now + conn, verdict, instrument_id=None, now=now, + account_id=self._account_id, ) for rule in self._rules: @@ -145,7 +150,8 @@ async def check( verdict = rule.check_symbol(instrument_id, now, side, balance) if verdict is not None: return await _record_and_build_rejection( - conn, verdict, instrument_id=instrument_id, now=now + conn, verdict, instrument_id=instrument_id, now=now, + account_id=self._account_id, ) return None @@ -167,6 +173,7 @@ async def _record_and_build_rejection( *, instrument_id: InstrumentId | None, now: datetime, + account_id: str | None, ) -> RiskRejection: """命中 rule → 写 ``risk_locks`` + 返 RiskRejection。""" market = verdict.lock_market @@ -185,6 +192,7 @@ async def _record_and_build_rejection( market=market, symbol=symbol, side=verdict.lock_side, + account_id=account_id, ) return RiskRejection( rule_name=verdict.rule_name, diff --git a/services/paper/src/inalpha_paper/execution/risk_guard_factory.py b/services/paper/src/inalpha_paper/execution/risk_guard_factory.py index b2cccacc..b57dde5b 100644 --- a/services/paper/src/inalpha_paper/execution/risk_guard_factory.py +++ b/services/paper/src/inalpha_paper/execution/risk_guard_factory.py @@ -17,8 +17,6 @@ 不在范围: - 持久化 cache(重启 → 空 cache,靠 lazy 重建) -- ``risk_locks`` 表加 account_id 列(当前 lock 仍是 venue+symbol scoped 全局共享; - 多账户 lock 隔离独立 issue) - ``starting_balance`` 仍从 config 读全局值(多账户独立余额留 D-10+) """ from __future__ import annotations @@ -123,6 +121,7 @@ async def get_for_check(self, account_id: UUID) -> RiskGuard: guard = RiskGuard( rules=rules, starting_balance=self._cfg.starting_balance, + account_id=str(account_id), ) self._cache[account_id] = (repo, guard) if len(self._cache) > self._cache_size: diff --git a/services/paper/src/inalpha_paper/market_annualization.py b/services/paper/src/inalpha_paper/market_annualization.py new file mode 100644 index 00000000..303e09d4 --- /dev/null +++ b/services/paper/src/inalpha_paper/market_annualization.py @@ -0,0 +1,69 @@ +"""Annualization derived from the generated exchange-calendar grid.""" +from __future__ import annotations + +import math +from datetime import datetime + +import exchange_calendars as xcals # type: ignore[import-untyped] +import pandas as pd # type: ignore[import-untyped] +from inalpha_shared.errors import ValidationError + +_MINUTES = { + "1m": 1, + "3m": 3, + "5m": 5, + "15m": 15, + "30m": 30, + "1h": 60, + "2h": 120, + "4h": 240, + "6h": 360, + "8h": 480, + "12h": 720, +} + + +def calendar_periods(code: str, timeframe: str, as_of: datetime) -> int: + """Count actual generated bars per year for an exchange calendar.""" + calendar = xcals.get_calendar(code) + end = pd.Timestamp(as_of) + end = end.tz_localize("UTC") if end.tz is None else end.tz_convert("UTC") + start = max(end - pd.DateOffset(years=3), calendar.first_session.tz_localize("UTC")) + schedule = calendar.schedule.loc[str(start.date()) : str(end.date())] + if schedule.empty: + raise ValidationError( + f"calendar {code} has no sessions near {end.isoformat()}", + code="EVOLUTION_MARKET_CALENDAR_EMPTY", + ) + years = max((end - start).total_seconds() / (365.2425 * 86_400), 1.0) + if timeframe == "1d": + count = len(schedule) + elif timeframe == "3d": + count = math.ceil(len(schedule) / 3) + elif timeframe == "1w": + count = len( + {(day.isocalendar().year, day.isocalendar().week) for day in schedule.index} + ) + elif timeframe == "1M": + count = len({(day.year, day.month) for day in schedule.index}) + else: + seconds = _MINUTES[timeframe] * 60 + count = sum(_session_count(row, seconds) for _, row in schedule.iterrows()) + return max(1, round(count / years)) + + +def _session_count(row: pd.Series, seconds: int) -> int: + segments = [(row["open"], row["close"])] + if not pd.isna(row["break_start"]) and not pd.isna(row["break_end"]): + segments = [ + (row["open"], row["break_start"]), + (row["break_end"], row["close"]), + ] + return sum( + math.ceil((end - start).total_seconds() / seconds) + for start, end in segments + if end > start + ) + + +__all__ = ["calendar_periods"] diff --git a/services/paper/src/inalpha_paper/market_cutoff.py b/services/paper/src/inalpha_paper/market_cutoff.py new file mode 100644 index 00000000..e77588e7 --- /dev/null +++ b/services/paper/src/inalpha_paper/market_cutoff.py @@ -0,0 +1,102 @@ +"""Derive completed bar timestamps from connector grids and calendars.""" +from __future__ import annotations + +import math +from datetime import UTC, datetime, timedelta + +import exchange_calendars as xcals # type: ignore[import-untyped] +import pandas as pd # type: ignore[import-untyped] +from inalpha_shared.errors import ValidationError + +from .market_evaluation import MarketEvaluationContext, fixed_timeframe_seconds +from .market_grid import calendar_bar_timestamps + + +def expected_latest_bar_open( + context: MarketEvaluationContext, + as_of: datetime, +) -> datetime: + """Return the latest completed timestamp supplied by the connector.""" + now = _utc(as_of) + if context.calendar_code is None: + timeframe = context.canonical_timeframe + if timeframe == "1M": + current = datetime(now.year, now.month, 1, tzinfo=UTC) + prior = current - timedelta(days=1) + return datetime(prior.year, prior.month, 1, tzinfo=UTC) + seconds = fixed_timeframe_seconds(timeframe) + assert seconds is not None + anchor = 4 * 86_400 if timeframe == "1w" else 0 + timestamp = ( + math.floor((now.timestamp() - anchor) / seconds) * seconds + + anchor + - seconds + ) + return datetime.fromtimestamp(timestamp, tz=UTC) + values = expected_bar_timestamps(context, now - timedelta(days=100), now) + if not values: + raise ValidationError( + "market calendar has no completed bar before as_of", + code="EVOLUTION_NO_CLOSED_BARS", + ) + return values[-1] + + +def expected_bar_timestamps( + context: MarketEvaluationContext, + start: datetime, + as_of: datetime, +) -> tuple[datetime, ...]: + """Return completed connector timestamps within the requested interval.""" + begin, now = _utc(start), _utc(as_of) + if context.calendar_code is None: + return _continuous_grid(context.canonical_timeframe, begin, now) + timeframe = context.canonical_timeframe + if timeframe == "3d": + raise ValidationError( + "unsupported calendar timeframe: 3d", + code="EVOLUTION_TIMEFRAME_UNSUPPORTED", + ) + calendar = xcals.get_calendar(context.calendar_code) + end = pd.Timestamp(now.date()) + pd.offsets.MonthEnd(1) + pd.Timedelta(days=7) + schedule = calendar.schedule.loc[ + str((begin - timedelta(days=35)).date()) : str(end.date()) + ] + values = calendar_bar_timestamps(calendar, schedule, context, now) + return tuple(value for value in values if begin <= value <= now) + + +def _continuous_grid( + timeframe: str, + start: datetime, + now: datetime, +) -> tuple[datetime, ...]: + if timeframe == "1M": + cursor = datetime(start.year, start.month, 1, tzinfo=UTC) + values: list[datetime] = [] + while True: + following = (pd.Timestamp(cursor) + pd.DateOffset(months=1)).to_pydatetime() + if following > now: + break + if cursor >= start: + values.append(cursor) + cursor = following + return tuple(values) + seconds = fixed_timeframe_seconds(timeframe) + assert seconds is not None + anchor = 4 * 86_400 if timeframe == "1w" else 0 + first = math.ceil((start.timestamp() - anchor) / seconds) * seconds + anchor + last = math.floor((now.timestamp() - anchor) / seconds) * seconds + anchor - seconds + return tuple( + datetime.fromtimestamp(timestamp, tz=UTC) + for timestamp in range(int(first), int(last) + 1, seconds) + ) + + +def _utc(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) + + +__all__ = ["expected_bar_timestamps", "expected_latest_bar_open"] diff --git a/services/paper/src/inalpha_paper/market_evaluation.py b/services/paper/src/inalpha_paper/market_evaluation.py new file mode 100644 index 00000000..4ddd1f9f --- /dev/null +++ b/services/paper/src/inalpha_paper/market_evaluation.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime + +from inalpha_shared.errors import ValidationError + +from .engine.metrics import periods_per_year +from .execution.risk_rules.exchange_resolver import is_crypto_venue, resolve_calendar_code +from .market_annualization import calendar_periods + +_ALIASES = {"1wk": "1w", "1mo": "1M"} +_MINUTES = { + "1m": 1, + "3m": 3, + "5m": 5, + "15m": 15, + "30m": 30, + "1h": 60, + "2h": 120, + "4h": 240, + "6h": 360, + "8h": 480, + "12h": 720, +} +_DAYS = {"1d": 1, "3d": 3} + + +@dataclass(frozen=True, slots=True) +class MarketEvaluationContext: + requested_timeframe: str + data_timeframe: str + canonical_timeframe: str + annualization_periods: int + calendar_code: str | None + connector: str + + +def build_market_evaluation_context( + *, + venue: str, + symbol: str, + timeframe: str, + as_of: datetime, +) -> MarketEvaluationContext: + canonical = canonical_timeframe(timeframe) + try: + crypto_periods = periods_per_year(canonical) + except ValueError as exc: + raise ValidationError( + f"unsupported evaluation timeframe: {timeframe}", + code="EVOLUTION_TIMEFRAME_UNSUPPORTED", + ) from exc + connector = venue.strip().lower() + if is_crypto_venue(venue): + return MarketEvaluationContext( + timeframe, timeframe, canonical, crypto_periods, None, connector + ) + if connector == "fred": + raise ValidationError( + "FRED series are not tradable evolution instruments", + code="EVOLUTION_MARKET_UNSUPPORTED", + ) + code = resolve_calendar_code(venue, symbol) + if code is None: + raise ValidationError( + f"cannot resolve market calendar for {symbol}@{venue}", + code="EVOLUTION_MARKET_CALENDAR_UNKNOWN", + ) + return MarketEvaluationContext( + timeframe, + timeframe, + canonical, + calendar_periods(code, canonical, as_of), + code, + connector, + ) + + +def canonical_timeframe(timeframe: str) -> str: + return _ALIASES.get(timeframe, timeframe) + + +def fixed_timeframe_seconds(timeframe: str) -> int | None: + canonical = canonical_timeframe(timeframe) + if canonical in _MINUTES: + return _MINUTES[canonical] * 60 + if canonical in _DAYS: + return _DAYS[canonical] * 86_400 + if canonical == "1w": + return 7 * 86_400 + if canonical == "1M": + return None + raise ValidationError( + f"unsupported evaluation timeframe: {timeframe}", + code="EVOLUTION_TIMEFRAME_UNSUPPORTED", + ) diff --git a/services/paper/src/inalpha_paper/market_grid.py b/services/paper/src/inalpha_paper/market_grid.py new file mode 100644 index 00000000..a7323a55 --- /dev/null +++ b/services/paper/src/inalpha_paper/market_grid.py @@ -0,0 +1,92 @@ +"""Calendar-specific bar label generation.""" +from __future__ import annotations + +import math +from datetime import UTC, datetime + +import pandas as pd # type: ignore[import-untyped] + +from .market_evaluation import MarketEvaluationContext, fixed_timeframe_seconds + +_CLOSE_LABEL_CONNECTORS = {"akshare", "baostock"} + + +def calendar_bar_timestamps( + calendar: object, + schedule: pd.DataFrame, + context: MarketEvaluationContext, + now: datetime, +) -> list[datetime]: + """Generate completed connector timestamps from a market schedule.""" + timeframe = context.canonical_timeframe + if timeframe in {"1d", "1w", "1M"}: + return _period_grid(calendar, schedule, context, now) + seconds = fixed_timeframe_seconds(timeframe) + assert seconds is not None + return _intraday_grid(schedule, context.connector, seconds, now) + + +def _period_grid( + calendar: object, + schedule: pd.DataFrame, + context: MarketEvaluationContext, + now: datetime, +) -> list[datetime]: + timeframe = context.canonical_timeframe + if timeframe == "1d": + keys = list(schedule.index) + elif timeframe == "1w": + keys = [(d.isocalendar().year, d.isocalendar().week) for d in schedule.index] + else: + keys = [(d.year, d.month) for d in schedule.index] + groups = schedule.groupby(pd.Series(keys, index=schedule.index)) + values: list[datetime] = [] + for _key, rows in groups: + if rows.iloc[-1]["close"] > pd.Timestamp(now): + continue + if context.connector in _CLOSE_LABEL_CONNECTORS: + label = rows.index[-1] + values.append(datetime(label.year, label.month, label.day, tzinfo=UTC)) + continue + first = rows.index[0] + if timeframe == "1w": + first -= pd.Timedelta(days=first.weekday()) + elif timeframe == "1M": + first = pd.Timestamp(first.year, first.month, 1) + values.append( + datetime( + first.year, + first.month, + first.day, + tzinfo=calendar.tz, # type: ignore[attr-defined] + ).astimezone(UTC) + ) + return values + + +def _intraday_grid( + schedule: pd.DataFrame, + connector: str, + seconds: int, + now: datetime, +) -> list[datetime]: + values: list[datetime] = [] + close_label = connector in _CLOSE_LABEL_CONNECTORS + for _label, row in schedule.iterrows(): + segments = [(row["open"], row["close"])] + if not pd.isna(row["break_start"]) and not pd.isna(row["break_end"]): + segments = [ + (row["open"], row["break_start"]), + (row["break_end"], row["close"]), + ] + for start, end in segments: + count = math.ceil((end - start).total_seconds() / seconds) + for index in range(count): + opened = start + pd.Timedelta(seconds=index * seconds) + closed = min(opened + pd.Timedelta(seconds=seconds), end) + if closed <= pd.Timestamp(now): + values.append((closed if close_label else opened).to_pydatetime()) + return values + + +__all__ = ["calendar_bar_timestamps"] diff --git a/services/paper/src/inalpha_paper/runner.py b/services/paper/src/inalpha_paper/runner.py index 7b73c14e..99117c8d 100644 --- a/services/paper/src/inalpha_paper/runner.py +++ b/services/paper/src/inalpha_paper/runner.py @@ -21,6 +21,7 @@ from inalpha_shared.errors import NotFoundError, ValidationError from psycopg import AsyncConnection +from .bar_conversion import bar_from_dict as _bar_from_dict from .config import get_paper_settings from .data_client import DataClient from .engine.backtest import BacktestEngine, CVReport, run_cv_backtest @@ -30,16 +31,16 @@ PurgedKFold, WalkForward, ) -from .engine.metrics import ( - bar_returns, - max_drawdown_pct, - periods_per_year, - sharpe_ratio, -) from .engine.pool import get_pool -from .engine.robustness import bootstrap_sharpe_ci -from .kernel.clock import datetime_to_ns +from .evaluation_metrics import ( + fitness_from_report as _fitness_from_report, +) +from .evaluation_metrics import ( + validation_from_report as _validation_from_report, +) +from .evaluation_worker import run_engine_worker as run_engine_in_subprocess from .kernel.identifiers import InstrumentId +from .market_evaluation import build_market_evaluation_context from .model.data import Bar from .schemas import ( BacktestRequest, @@ -51,21 +52,14 @@ PositionSnapshot, SharpeCI, ValidationBlock, - ValidationSegment, ) from .storage import backtest_runs as backtest_runs_store from .storage import backtest_trades as backtest_trades_store from .storage import strategy_candidates as candidates_store from .strategies import BASELINE_BUY_AND_HOLD, get_strategy_class from .strategy.base import Strategy -from .strategy_authoring import ( - FitnessInputs, - audit_strategy_code, - calmar_from_report, - compose_fitness, - load_strategy_class, - verify_strategy_contract, -) +from .strategy_authoring import audit_strategy_code, load_strategy_class +from .strategy_evaluation import evaluate_buy_and_hold, evaluate_strategy_source if TYPE_CHECKING: from concurrent.futures import ProcessPoolExecutor @@ -91,6 +85,12 @@ async def run_backtest( 落库失败不阻断回测返回(warning log)—— D-8b' 容错原则。 """ started_at = datetime.now(tz=UTC) + market_context = build_market_evaluation_context( + venue=req.venue, + symbol=req.symbol, + timeframe=req.timeframe, + as_of=req.to_ts, + ) # 1. 拉数据 —— DataClient.get_bars 默认 fresh=True:先 POST /backfill/bars # 把 to_ts 之前最新 K 线补齐,再 GET /bars。新 symbol / 新窗口在这一步自愈。 raw_bars = await data_client.get_bars( @@ -102,7 +102,10 @@ async def run_backtest( ) instrument_id = InstrumentId(symbol=req.symbol, venue=req.venue) - bars = [_bar_from_dict(b, instrument_id, req.timeframe) for b in raw_bars] + bars = [ + _bar_from_dict(b, instrument_id, market_context.canonical_timeframe) + for b in raw_bars + ] if not bars: raise ValidationError( f"data-service returned 0 bars for {req.symbol}@{req.venue} " @@ -134,13 +137,6 @@ async def run_backtest( code="CANDIDATE_NOT_FOUND", ) candidate_code = candidate_row["code"] - # 二次 AST 审计 —— 即使入库时已审过,跑回测前再审一次 - reaudit = audit_strategy_code(candidate_code) - if not reaudit.ok: - raise ValidationError( - f"candidate {req.candidate_id} failed re-audit: {reaudit.reason()}", - code="CANDIDATE_REAUDIT_FAILED", - ) # 用作落库 / 响应的 strategy_code("candidate:" 与内置 ID 同字段区分) effective_strategy_code = ( @@ -153,48 +149,54 @@ async def run_backtest( # 优先级:pool 已起(生产)→ 走 pool;未起(旧测试 / 同步入口)→ 同进程跑兜底 # D-9 candidate 路径:同时并发跑一次 buy_and_hold baseline 做 alpha 对照 # (pool 已起时真并行;未起时退化为顺序但语义不变) - candidate_task = _run_engine( - bars=bars, - instrument_id=instrument_id, - timeframe=req.timeframe, - strategy_id=req.strategy_id, - candidate_code=candidate_code, - params=req.params, - initial_cash=req.initial_cash, - fee_rate=req.fee_rate, - # perp 透传:做空策略须在 perp 回测才能真做空(spot 下裸空被守门拒=0 成交)。 - # baseline(下方 buy_and_hold)保持 spot,作 alpha 对照锚点。 - trading_mode=req.trading_mode, - leverage=req.leverage, - funding_rate=req.funding_rate, - ) + bars_per_year = float(market_context.annualization_periods) + source_evaluation = None + baseline_evaluation = None try: - if req.candidate_id is not None: - # baseline = "first bar all-in 持有到收盘"。BuyAndHoldStrategy 在 - # bars[0] 发单 → bars[1] 才撮合(避免 lookahead)。撮合层 cash 守门 - # 用的是 bars[1].open,所以预算 qty 也用 bars[1].open;fee_rate + - # 0.5% 容忍连续两根 bar 间的价格 jitter(一般 < 1%)。 - fill_open = bars[1].open if len(bars) > 1 else bars[0].open - if fill_open <= 0: - raise ValidationError( - f"bar open price must be positive, got {fill_open} " - f"for {req.symbol}@{req.venue}; data may be corrupt", - code="INVALID_BAR_PRICE", - ) - baseline_qty = req.initial_cash / fill_open / (1.0 + req.fee_rate + 0.005) - baseline_task = _run_engine( + if candidate_code is not None: + source_evaluation, baseline_evaluation = await asyncio.gather( + evaluate_strategy_source( + source_code=candidate_code, + bars=bars, + instrument_id=instrument_id, + timeframe=market_context.canonical_timeframe, + run_engine=_run_engine, + params=req.params, + initial_cash=req.initial_cash, + fee_rate=req.fee_rate, + validation_split=req.validation_split, + annualization_periods=bars_per_year, + trading_mode=req.trading_mode, + leverage=req.leverage, + funding_rate=req.funding_rate, + ), + evaluate_buy_and_hold( + bars=bars, + instrument_id=instrument_id, + timeframe=market_context.canonical_timeframe, + run_engine=_run_engine, + initial_cash=req.initial_cash, + fee_rate=req.fee_rate, + annualization_periods=bars_per_year, + ), + ) + report = source_evaluation.report + baseline_report = baseline_evaluation.report + else: + report = await _run_engine( bars=bars, instrument_id=instrument_id, - timeframe=req.timeframe, - strategy_id=BASELINE_BUY_AND_HOLD, + timeframe=market_context.canonical_timeframe, + strategy_id=req.strategy_id, candidate_code=None, - params={"trade_size": baseline_qty}, + params=req.params, initial_cash=req.initial_cash, fee_rate=req.fee_rate, + trading_mode=req.trading_mode, + leverage=req.leverage, + funding_rate=req.funding_rate, + annualization_periods=market_context.annualization_periods, ) - report, baseline_report = await asyncio.gather(candidate_task, baseline_task) - else: - report = await candidate_task baseline_report = None except (AttributeError, TypeError, ValueError, KeyError, IndexError, ZeroDivisionError) as exc: # D-9 · candidate 路径策略源码运行时错(字段名 / 类型 / 数学)→ 翻 422 让 agent @@ -238,16 +240,23 @@ async def run_backtest( finished_at = datetime.now(tz=UTC) # 5. 计算 fitness(D-9 · ADR-0020 E1:多目标合成,不允许裸 Sharpe 排序候选) - bars_per_year = float(periods_per_year(req.timeframe)) - fitness_value = _fitness_from_report(report, bars_per_year=bars_per_year) + fitness_value = ( + source_evaluation.snapshot.fitness + if source_evaluation is not None + else _fitness_from_report(report, bars_per_year=bars_per_year) + ) # calmar 指标落库一律取 report.calmar —— 不再走 calmar_from_report 第二条 # 计算路径,否则 fitness 侧公式一变,candidates 与 backtest_runs 两表的 # calmar 会静默分叉。 # 5a'. D-12:holdout 时间切分验证(单次运行按曲线切段,不二次跑引擎)。 # baseline 不切段——alpha 对照仍看全窗。 - validation: ValidationBlock | None = None - if req.validation_split > 0: + validation: ValidationBlock | None = ( + source_evaluation.snapshot.validation + if source_evaluation is not None + else None + ) + if source_evaluation is None and req.validation_split > 0: validation = _validation_from_report( report, split=req.validation_split, bars_per_year=bars_per_year ) @@ -255,8 +264,12 @@ async def run_backtest( # 5b. D-9 candidate 路径:组装 baseline 对照(buy_and_hold 同 symbol/timeframe/period) baseline_snapshot: BaselineSnapshot | None = None if baseline_report is not None: - baseline_fitness = _fitness_from_report( - baseline_report, bars_per_year=bars_per_year + baseline_fitness = ( + baseline_evaluation.snapshot.fitness + if baseline_evaluation is not None + else _fitness_from_report( + baseline_report, bars_per_year=bars_per_year + ) ) baseline_snapshot = BaselineSnapshot( strategy_id=BASELINE_BUY_AND_HOLD, @@ -378,99 +391,6 @@ async def run_backtest( ) -def _validation_from_report( - report: Any, - *, - split: float, - bars_per_year: float, -) -> ValidationBlock | None: - """单次回测结果按时间切 train/holdout 两段算指标(D-12 · 纯函数)。 - - 切的是 ``report.equity_curve``(全量,未降采样)——不二次跑引擎、不重拉数据、 - 不改变 run 记录语义。holdout 段衰减比 = holdout_sharpe / train_sharpe,是 - "这套参数是不是只在前半窗有效"的最便宜信号。 - - **不是盲 OOS**:调用方(agent)看得到 holdout 指标,反复对着它调参会间接 - 过拟合;纪律约束在 orchestrator prompt(调参看 train,holdout 只作裁判)。 - - Returns: - 曲线 < 10 点或任一段 < 2 点(切不出有意义的段)时返 ``None``。 - """ - curve: list[tuple[int, float]] = report.equity_curve - if len(curve) < 10: - return None - split_idx = int(len(curve) * split) - if split_idx < 2 or len(curve) - split_idx < 2: - return None - - cut_ts_ns = curve[split_idx][0] - values = [eq for _ts, eq in curve] - train_vals = values[:split_idx] - # holdout 段带上切点前一根作收益率基准(首根 holdout return 需要前值) - holdout_vals = values[split_idx - 1 :] - - fills = list(getattr(report, "fills", []) or []) - train_fills = sum(1 for f in fills if f.ts_ns < cut_ts_ns) - holdout_fills = len(fills) - train_fills - - def _segment( - vals: list[float], num_trades: int, *, bar_count: int | None = None - ) -> ValidationSegment: - rets = bar_returns(vals) - return ValidationSegment( - sharpe=sharpe_ratio(rets, int(bars_per_year)), - total_return_pct=(vals[-1] / vals[0] - 1.0) * 100.0 if vals[0] > 0 else 0.0, - max_drawdown_pct=max_drawdown_pct(vals), - num_trades=num_trades, - num_bars=bar_count if bar_count is not None else len(vals), - ) - - train_seg = _segment(train_vals, train_fills) - # holdout_vals 多带切点前一根作收益率基准 → num_bars 报真实 holdout 段长(len-1), - # 否则 insufficient_sample 的 <30 判据在真实 29 根时被 +1 顶到 30 漏报(CR #86)。 - holdout_seg = _segment( - holdout_vals, holdout_fills, bar_count=len(holdout_vals) - 1 - ) - - flags: list[str] = [] - # holdout 段自身要有足够样本:用 holdout_seg.num_trades 而非全窗口 report.num_trades - # ——策略可能 train 段交易密集、holdout 段几乎不动,全窗口 ≥5 却 holdout 只 1-2 笔, - # 此时 holdout Sharpe / decay_ratio 不可信,必须 flag(CR #86 major)。 - if ( - holdout_seg.num_bars < 30 - or holdout_seg.num_trades < 2 - or report.num_trades < 5 - ): - flags.append("insufficient_sample") - - decay_ratio: float | None = None - if train_seg.sharpe is None or holdout_seg.sharpe is None: - flags.append("sharpe_undefined") - elif train_seg.sharpe <= 0: - # train 段本身就不赚:衰减比无意义(负除负会假装"没衰减") - flags.append("train_sharpe_nonpositive") - else: - decay_ratio = holdout_seg.sharpe / train_seg.sharpe - - ci_includes_zero: bool | None = None - holdout_rets = bar_returns(holdout_vals) - if len(holdout_rets) >= 30: - try: - ci = bootstrap_sharpe_ci(holdout_rets, n_samples=1000) - ci_includes_zero = ci.ci_includes_zero - except Exception: - logger.warning("bootstrap_sharpe_ci failed", exc_info=True) - - return ValidationBlock( - split_ratio=split, - train=train_seg, - holdout=holdout_seg, - decay_ratio=decay_ratio, - holdout_sharpe_ci_includes_zero=ci_includes_zero, - flags=flags, - ) - - async def _persist_run( *, conn: AsyncConnection, @@ -555,28 +475,6 @@ async def _persist_run( return None -def _fitness_from_report(report: Any, *, bars_per_year: float) -> float: - """从 ``BacktestReport`` 算多目标 fitness(D-9 · ADR-0020 §适应度函数)。 - - 抽出公用函数避免主路径 / baseline 路径重复算 calmar + compose_fitness。 - """ - calmar = calmar_from_report( - total_return_pct=report.total_return_pct, - max_drawdown_pct=report.max_drawdown_pct, - num_bars_processed=report.num_bars_processed, - bars_per_year=bars_per_year, - ) - return compose_fitness( - FitnessInputs( - sharpe=report.sharpe, - calmar=calmar, - max_drawdown_pct=report.max_drawdown_pct, - num_trades=report.num_trades, - num_bars_processed=report.num_bars_processed, - ) - ) - - async def run_cv( req: CVBacktestRequest, data_client: DataClient, @@ -789,6 +687,7 @@ async def _run_engine( trading_mode: str = "spot", leverage: int = 1, funding_rate: float = 0.0, + annualization_periods: int | None = None, ) -> BacktestReport: """调度 engine 执行:pool 已起则丢 ProcessPool,未起则同进程跑兜底。 @@ -824,6 +723,7 @@ async def _run_engine( trading_mode=trading_mode, leverage=leverage, funding_rate=funding_rate, + annualization_periods=annualization_periods, ) loop = asyncio.get_running_loop() @@ -846,6 +746,7 @@ async def _run_engine( trading_mode=trading_mode, leverage=leverage, funding_rate=funding_rate, + annualization_periods=annualization_periods, ) return await loop.run_in_executor(pool, fn) @@ -883,6 +784,7 @@ def _make_pool_call( trading_mode: str = "spot", leverage: int = 1, funding_rate: float = 0.0, + annualization_periods: int | None = None, ) -> Any: """生成一个无参 callable,丢给 ``run_in_executor``。 @@ -909,126 +811,5 @@ def _make_pool_call( trading_mode=trading_mode, leverage=leverage, funding_rate=funding_rate, - ) - - -def run_engine_in_subprocess( - *, - bars: list[Bar], - instrument_id: InstrumentId, - timeframe: str, - strategy_id: str | None, - params: dict[str, Any], - initial_cash: float, - fee_rate: float, - candidate_code: str | None = None, - protective_stop_loss_pct: float | None = None, - protective_take_profit_pct: float | None = None, - protective_trailing_stop_pct: float | None = None, - protective_chandelier_atr_mult: float | None = None, - protective_chandelier_atr_period: int = 22, - trading_mode: str = "spot", - leverage: int = 1, - funding_rate: float = 0.0, -) -> BacktestReport: - """**Top-level 函数 = 可 pickle**:实例化 engine + strategy + 跑 bars,返 ``BacktestReport``。 - - 在子进程里跑(ADR-0025 §D1): - - - 不调任何 async / httpx / DB —— 全部 IO 留在 main 协程 - - 只接受 picklable 输入(dataclass + 原生 dict / list) - - raise 直接通过 future 传回 main,main 翻成 HTTP 5xx 结构化错误 - - rlimit / CPU 超时由 ``engine.pool._worker_init`` 在 worker 启动时设置 - - 放在 runner.py 而不是 pool.py:保持调用现场的 import 上下文(``BacktestEngine`` 之类 - 模块在 worker 第一次 fork 后由 pickle 反序列化时按需 import)。 - - D-9 · candidate 分支: - - ``candidate_code`` 非空 → ``dynamic_loader`` + ``verify_strategy_contract`` 在子进程内跑 - (main 进程已审过;这里只走 load + contract,避免再次 AST 浪费 CPU) - - 否则走内置 ``get_strategy_class(strategy_id)`` - """ - # ADR-0052:框架级持仓保护止损阈值由 main 进程从 Settings 解析后传入(子进程不读 - # Settings 单例,保持 picklable + 纯)。三阈值全 None → BacktestEngine 不建 guard。 - engine = BacktestEngine( - initial_cash=initial_cash, - fee_rate=fee_rate, - protective_stop_loss_pct=protective_stop_loss_pct, - protective_take_profit_pct=protective_take_profit_pct, - protective_trailing_stop_pct=protective_trailing_stop_pct, - protective_chandelier_atr_mult=protective_chandelier_atr_mult, - protective_chandelier_atr_period=protective_chandelier_atr_period, - trading_mode=trading_mode, - leverage=leverage, - funding_rate=funding_rate, - ) - - if candidate_code is not None: - strategy_cls = load_strategy_class(candidate_code) - verify_strategy_contract(strategy_cls) - strategy_name = f"{strategy_cls.__name__}-{instrument_id.symbol}" - else: - if strategy_id is None: - raise ValidationError( - "internal: neither strategy_id nor candidate_code provided", - code="STRATEGY_MISSING", - ) - strategy_cls = get_strategy_class(strategy_id) - strategy_name = f"{strategy_id}-{instrument_id.symbol}" - - # strategy 子类构造签名不一(SMA cross 要 lookback、mean_rev 要 threshold 等); - # MVP 不抽 strategy factory,**kwargs 喂参数 + type:ignore - # 自动注入 initial_cash:strategy `__init__` 接受这个字段(sma_cross / - # mean_reversion 的 position_pct 路径)就传;不接受的老 strategy 不传。 - strategy_kwargs: dict[str, Any] = dict(params) - try: - sig = inspect.signature(strategy_cls.__init__) - if "initial_cash" in sig.parameters and "initial_cash" not in strategy_kwargs: - strategy_kwargs["initial_cash"] = initial_cash - except (TypeError, ValueError): - pass - try: - sig = inspect.signature(strategy_cls.__init__) - if "position_pct" in sig.parameters and "position_pct" not in strategy_kwargs: - strategy_kwargs["position_pct"] = 1.0 - except (TypeError, ValueError): - pass - strategy = strategy_cls( # type: ignore[call-arg] - name=strategy_name, - clock=engine.clock, - msgbus=engine.msgbus, - instrument_id=instrument_id, - timeframe=timeframe, - **strategy_kwargs, - ) - engine.add_strategy(strategy) - return engine.run(bars) - - -# datetime → ns 整数转换已上移 kernel.clock.datetime_to_ns(report.py 也要用, -# 留在 runner 会形成 engine → runner 反向依赖)。 - - -def _bar_from_dict(d: dict[str, Any], instrument_id: InstrumentId, timeframe: str) -> Bar: - """data-service ``BarResponse`` dict → 内核 ``Bar`` dataclass。""" - # ts 字段 data-service 返 ISO datetime 字符串 - ts_str = d["ts"] - if isinstance(ts_str, str): - dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00")) - else: - dt = ts_str - if dt.tzinfo is None: - dt = dt.replace(tzinfo=UTC) - ts_ns = datetime_to_ns(dt) - - return Bar( - instrument_id=instrument_id, - timeframe=timeframe, - open=float(d["open"]), - high=float(d["high"]), - low=float(d["low"]), - close=float(d["close"]), - volume=float(d["volume"]), - ts_event=ts_ns, - ts_init=ts_ns, + annualization_periods=annualization_periods, ) diff --git a/services/paper/src/inalpha_paper/storage/risk_locks.py b/services/paper/src/inalpha_paper/storage/risk_locks.py index 13371161..0d06e96c 100644 --- a/services/paper/src/inalpha_paper/storage/risk_locks.py +++ b/services/paper/src/inalpha_paper/storage/risk_locks.py @@ -27,6 +27,7 @@ async def insert( market: str | None = None, symbol: str | None = None, side: str = "*", + account_id: str | None = None, ) -> int: """写一行 lock。返回新 id。 @@ -35,18 +36,19 @@ async def insert( market: scope='market' 必填;scope='symbol' 可选(便于按 market 聚合) symbol: scope='symbol' 必填 side: 'long' / 'short' / '*' + account_id: 账户归属(UUID 字符串);None 表示非账户级(backtest reconcile 路径) """ async with conn.cursor() as cur: await cur.execute( """ INSERT INTO risk_locks ( - scope, market, symbol, side, rule_name, reason, locked_until + scope, market, symbol, side, rule_name, reason, locked_until, account_id ) VALUES ( - %s, %s, %s, %s, %s, %s, %s + %s, %s, %s, %s, %s, %s, %s, %s ) RETURNING id """, - (scope, market, symbol, side, rule_name, reason, locked_until), + (scope, market, symbol, side, rule_name, reason, locked_until, account_id), ) row = await cur.fetchone() return int(row["id"]) # type: ignore[index] @@ -59,10 +61,12 @@ async def list_active( scope: str | None = None, market: str | None = None, symbol: str | None = None, + account_id: str | None = None, limit: int = 100, ) -> list[dict[str, Any]]: """列出 `now` 时刻仍有效的 lock(active=TRUE 且 locked_until > now)。 + ``account_id`` 提供时按账户过滤(多租户隔离);None 表示不过滤(测试 / backtest)。 按 locked_until DESC 排(解锁时间最远的先看)。 """ sql = ( @@ -71,6 +75,9 @@ async def list_active( "WHERE active = TRUE AND locked_until > %s" ) params: list[Any] = [now] + if account_id is not None: + sql += " AND account_id = %s" + params.append(account_id) if scope is not None: sql += " AND scope = %s" params.append(scope) @@ -92,6 +99,7 @@ async def list_active( async def list_recent( conn: AsyncConnection, *, + account_id: str | None = None, limit: int = 50, ) -> list[dict[str, Any]]: """列**最近**的风控锁——含已过期 / 已解锁的行(``active`` 不过滤),按 ``locked_at`` DESC。 @@ -107,15 +115,23 @@ async def list_recent( 时效过期但 reconciler 尚未跑 ``expire_past_locks`` 的锁,DB 原始 ``active`` 仍是 TRUE, 原样返回会让历史面板把已过期锁误显为生效中。人工解锁 vs 时效过期由 ``unlocked_by`` / ``unlock_reason`` 区分,不靠 ``active``。 + + ``account_id`` 提供时按账户过滤(多租户隔离);None 表示不过滤(测试 / backtest)。 """ sql = ( "SELECT id, scope, market, symbol, side, rule_name, reason, " "locked_at, locked_until, (active AND locked_until > NOW()) AS active, " "unlocked_at, unlocked_by, unlock_reason " - "FROM risk_locks ORDER BY locked_at DESC LIMIT %s" + "FROM risk_locks" ) + params: list[Any] = [] + if account_id is not None: + sql += " WHERE account_id = %s" + params.append(account_id) + sql += " ORDER BY locked_at DESC LIMIT %s" + params.append(limit) async with conn.cursor() as cur: - await cur.execute(sql, (limit,)) + await cur.execute(sql, tuple(params)) rows = await cur.fetchall() return list(rows) # type: ignore[arg-type] @@ -128,6 +144,7 @@ async def is_locked( market: str | None = None, symbol: str | None = None, side: str = "*", + account_id: str | None = None, ) -> dict[str, Any] | None: """精确查 `now` 时 ``scope`` + ``market``/``symbol`` 是否被锁。 @@ -136,6 +153,9 @@ async def is_locked( - 查询的 ``side='*'`` 命中任何方向的锁 - 否则 long 锁拦 long 查询、short 锁拦 short 查询,不互拦 + ``account_id`` 提供时按账户过滤——跨账户锁互不可见(多租户隔离);None 表示 + 不过滤(测试 / backtest)。 + Returns: 首个命中锁的 row dict(按 locked_until DESC,最远解锁的先看);无锁返 ``None``。 @@ -149,6 +169,9 @@ async def is_locked( "WHERE active = TRUE AND locked_until > %s AND scope = %s" ) params: list[Any] = [now, scope] + if account_id is not None: + sql += " AND account_id = %s" + params.append(account_id) if scope == "global": # global 锁不区分 market/symbol;这两个字段在 DB 里也是 NULL @@ -188,24 +211,29 @@ async def manual_unlock( *, unlocked_by: str, unlock_reason: str, + account_id: str | None = None, ) -> bool: """人工 unlock。软删(active=FALSE + 写入 unlock_at / unlocked_by / unlock_reason)。 + ``account_id`` 提供时限定只能解锁本账户的锁(多租户隔离);None 表示不过滤。 + Returns: - True 如果有行被改;False 如果 lock_id 不存在或已 inactive。 + True 如果有行被改;False 如果 lock_id 不存在、已 inactive、或不属该账户。 + """ + sql = """ + UPDATE risk_locks + SET active = FALSE, + unlocked_at = NOW(), + unlocked_by = %s, + unlock_reason = %s + WHERE id = %s AND active = TRUE """ + params: list[Any] = [unlocked_by, unlock_reason, lock_id] + if account_id is not None: + sql += " AND account_id = %s" + params.append(account_id) async with conn.cursor() as cur: - await cur.execute( - """ - UPDATE risk_locks - SET active = FALSE, - unlocked_at = NOW(), - unlocked_by = %s, - unlock_reason = %s - WHERE id = %s AND active = TRUE - """, - (unlocked_by, unlock_reason, lock_id), - ) + await cur.execute(sql, tuple(params)) return cur.rowcount > 0 diff --git a/services/paper/src/inalpha_paper/storage/strategy_candidates.py b/services/paper/src/inalpha_paper/storage/strategy_candidates.py index 39a0c1b0..dea91576 100644 --- a/services/paper/src/inalpha_paper/storage/strategy_candidates.py +++ b/services/paper/src/inalpha_paper/storage/strategy_candidates.py @@ -171,6 +171,27 @@ async def get_candidate( return _row_to_dict(row) if row else None +async def get_owned_candidate( + conn: AsyncConnection, + candidate_id: UUID, + owner_account_id: UUID, +) -> dict[str, Any] | None: + """按 candidate 与 owner 原子取完整行;跨 owner 与不存在均返 None。""" + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT id, code, code_hash, description, author, author_id, + owner_account_id, status, metrics, fitness, last_backtest_run_id, + audit, factor_snapshot, created_at, updated_at + FROM strategy_candidates + WHERE id = %s AND owner_account_id = %s + """, + (str(candidate_id), str(owner_account_id)), + ) + row = await cur.fetchone() + return _row_to_dict(row) if row else None + + async def list_candidates( conn: AsyncConnection, *, diff --git a/services/paper/src/inalpha_paper/strategy_evaluation.py b/services/paper/src/inalpha_paper/strategy_evaluation.py new file mode 100644 index 00000000..fd723fe0 --- /dev/null +++ b/services/paper/src/inalpha_paper/strategy_evaluation.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from inalpha_shared.errors import ValidationError + +from .engine.metrics import periods_per_year +from .evaluation_metrics import fitness_from_report, validation_from_report +from .evaluation_snapshot import EvaluationSnapshot +from .strategies import BASELINE_BUY_AND_HOLD +from .strategy_preparation import audit_strategy_source + +if TYPE_CHECKING: + from .engine.report import BacktestReport + from .kernel.identifiers import InstrumentId + from .model.data import Bar + +EngineRunner = Callable[..., Awaitable["BacktestReport"]] + + +@dataclass(frozen=True, slots=True) +class SourceEvaluation: + report: BacktestReport + snapshot: EvaluationSnapshot + + +async def evaluate_strategy_source( + *, + source_code: str, + bars: list[Bar], + instrument_id: InstrumentId, + timeframe: str, + run_engine: EngineRunner, + params: dict[str, Any] | None = None, + initial_cash: float = 10_000.0, + fee_rate: float = 0.001, + validation_split: float = 0.3, + annualization_periods: float | None = None, + trading_mode: str = "spot", + leverage: int = 1, + funding_rate: float = 0.0, +) -> SourceEvaluation: + """审计临时源码并在调用方提供的隔离执行器中评估。""" + _validate_bars(bars) + audited_source = audit_strategy_source(source_code) + periods = annualization_periods or float(periods_per_year(timeframe)) + report = await run_engine( + bars=bars, + instrument_id=instrument_id, + timeframe=timeframe, + strategy_id=None, + candidate_code=audited_source, + params=params or {}, + initial_cash=initial_cash, + fee_rate=fee_rate, + trading_mode=trading_mode, + leverage=leverage, + funding_rate=funding_rate, + annualization_periods=int(periods), + ) + validation, fitness = await asyncio.to_thread( + _compute_metrics, + report, + validation_split, + periods, + ) + return SourceEvaluation( + report=report, + snapshot=EvaluationSnapshot.from_report( + report, + fitness=fitness, + annualization_periods=periods, + validation=validation, + ), + ) + + +async def evaluate_buy_and_hold( + *, + bars: list[Bar], + instrument_id: InstrumentId, + timeframe: str, + run_engine: EngineRunner, + initial_cash: float = 10_000.0, + fee_rate: float = 0.001, + annualization_periods: float | None = None, +) -> SourceEvaluation: + """在同一冻结数据集上评估 buy-and-hold 市场基准。""" + _validate_bars(bars) + fill_open = bars[1].open + if fill_open <= 0: + raise ValidationError( + f"bar open price must be positive, got {fill_open}", + code="INVALID_BAR_PRICE", + ) + quantity = initial_cash / fill_open / (1.0 + fee_rate + 0.005) + periods = annualization_periods or float(periods_per_year(timeframe)) + report = await run_engine( + bars=bars, + instrument_id=instrument_id, + timeframe=timeframe, + strategy_id=BASELINE_BUY_AND_HOLD, + candidate_code=None, + params={"trade_size": quantity}, + initial_cash=initial_cash, + fee_rate=fee_rate, + annualization_periods=int(periods), + ) + fitness = fitness_from_report(report, bars_per_year=periods) + return SourceEvaluation( + report=report, + snapshot=EvaluationSnapshot.from_report( + report, + fitness=fitness, + annualization_periods=periods, + validation=None, + ), + ) + + +def _compute_metrics( + report: BacktestReport, + validation_split: float, + periods: float, +) -> tuple[Any, float]: + """在线程中计算 bootstrap/fitness,避免阻塞 async API。""" + validation = ( + validation_from_report(report, split=validation_split, bars_per_year=periods) + if validation_split > 0 + else None + ) + return validation, fitness_from_report(report, bars_per_year=periods) + + +def _validate_bars(bars: list[Bar]) -> None: + if len(bars) < 2: + raise ValidationError( + f"strategy evaluation needs >= 2 bars, got {len(bars)}", + code="NO_BARS_AVAILABLE", + ) diff --git a/services/paper/src/inalpha_paper/strategy_preparation.py b/services/paper/src/inalpha_paper/strategy_preparation.py new file mode 100644 index 00000000..88bc096d --- /dev/null +++ b/services/paper/src/inalpha_paper/strategy_preparation.py @@ -0,0 +1,57 @@ +"""临时策略源码的审计、加载与契约准备。""" + +from __future__ import annotations + +from dataclasses import dataclass + +from inalpha_shared.errors import ValidationError + +from .strategy_authoring import ( + ContractError, + DynamicLoadError, + audit_strategy_code, + load_strategy_class, + verify_strategy_contract, +) + + +@dataclass(frozen=True, slots=True) +class PreparedStrategy: + """通过审计、加载与契约校验的源码快照。""" + + source_code: str + class_name: str + + +def audit_strategy_source(source_code: str) -> str: + """只执行不会加载或运行候选代码的 AST 静态审计。""" + audit = audit_strategy_code(source_code) + if not audit.ok: + raise ValidationError( + f"strategy source failed audit: {audit.reason()}", + code="CANDIDATE_REAUDIT_FAILED", + ) + return source_code + + +def prepare_strategy_source(source_code: str) -> PreparedStrategy: + """执行源码审计、受限加载和策略契约校验。""" + audit_strategy_source(source_code) + try: + strategy_cls = load_strategy_class(source_code) + except DynamicLoadError as exc: + raise ValidationError( + f"strategy source failed to load: {exc}", + code="CANDIDATE_LOAD_FAILED", + ) from exc + try: + verify_strategy_contract(strategy_cls) + except ContractError as exc: + raise ValidationError( + f"strategy source failed contract check: {exc}", + code="CANDIDATE_CONTRACT_FAILED", + ) from exc + return PreparedStrategy(source_code=source_code, class_name=strategy_cls.__name__) + + +__all__ = ["PreparedStrategy", "audit_strategy_source", "prepare_strategy_source"] diff --git a/services/paper/tests/test_api_risk.py b/services/paper/tests/test_api_risk.py index 7539d3e1..0cefef81 100644 --- a/services/paper/tests/test_api_risk.py +++ b/services/paper/tests/test_api_risk.py @@ -16,11 +16,16 @@ from fastapi.testclient import TestClient from inalpha_shared.db import get_conn +from inalpha_paper.account_id import account_id_from_sub from inalpha_paper.api.risk import _reset_config_cache from inalpha_paper.execution.risk_rules import RiskRulesConfig pytestmark = pytest.mark.integration +# auth_headers fixture 的默认 sub="test-user";API 按 account_id_from_user(user) 过滤, +# 故测试要能看到锁,insert 时得用同一个 account_id。 +TEST_ACCOUNT = str(account_id_from_sub("test-user")) + _RISK_LOCKS_DDL = """ CREATE TABLE IF NOT EXISTS risk_locks ( @@ -38,7 +43,8 @@ active BOOLEAN NOT NULL DEFAULT TRUE, unlocked_at TIMESTAMPTZ, unlocked_by TEXT, - unlock_reason TEXT + unlock_reason TEXT, + account_id TEXT ) """ @@ -56,7 +62,7 @@ async def risk_locks_table(client: TestClient) -> AsyncIterator[None]: """Ensure `risk_locks` 表存在 + 测试前后清空(独立于 alembic 命令行)。 依赖 client fixture 拉起 DB pool。CREATE TABLE IF NOT EXISTS 幂等。 - DDL 必须与 `infra/migrations/versions/0006_risk_locks.py` 保持一致。 + DDL 必须与 `infra/migrations/versions/0040_risk_locks_account_id.py`(含 account_id)保持一致。 """ del client # 仅作 lifespan 依赖 async with get_conn() as conn: @@ -148,6 +154,7 @@ async def test_get_locks_returns_inserted_rows( market="binance", symbol="BTC/USDT@binance", side="*", + account_id=TEST_ACCOUNT, ) await conn.commit() @@ -175,10 +182,12 @@ async def test_get_locks_filter_by_scope( async with get_conn() as conn: await locks_store.insert( conn, scope="global", rule_name="R1", reason="g", locked_until=until, + account_id=TEST_ACCOUNT, ) await locks_store.insert( conn, scope="symbol", rule_name="R2", reason="s", locked_until=until, market="binance", symbol="BTC/USDT@binance", + account_id=TEST_ACCOUNT, ) await conn.commit() @@ -189,6 +198,33 @@ async def test_get_locks_filter_by_scope( assert body["locks"][0]["scope"] == "global" +@pytest.mark.asyncio +async def test_get_locks_isolated_by_account( + client: TestClient, auth_headers: dict[str, str], risk_locks_table: None +) -> None: + """跨账户隔离:其他账户的锁不可见,本账户的锁可见。""" + del risk_locks_table + from inalpha_paper.storage import risk_locks as locks_store + + until = datetime.now(UTC) + timedelta(hours=1) + async with get_conn() as conn: + await locks_store.insert( + conn, scope="global", rule_name="OtherRule", reason="他人锁", + locked_until=until, account_id="someone-else", + ) + await locks_store.insert( + conn, scope="global", rule_name="MyRule", reason="本账户锁", + locked_until=until, account_id=TEST_ACCOUNT, + ) + await conn.commit() + + r = client.get("/risk/locks", headers=auth_headers) + assert r.status_code == 200 + locks = r.json()["locks"] + assert len(locks) == 1 + assert locks[0]["rule_name"] == "MyRule" + + # ─── GET /risk/locks/history ─── @@ -205,11 +241,13 @@ async def test_get_locks_history_includes_expired_and_unlocked( await locks_store.insert( conn, scope="global", rule_name="ActiveRule", reason="生效", locked_until=now + timedelta(hours=1), + account_id=TEST_ACCOUNT, ) await locks_store.insert( conn, scope="symbol", rule_name="CooldownRule", reason="冷却过期", locked_until=now - timedelta(minutes=1), market="binance", symbol="BTC/USDT@binance", + account_id=TEST_ACCOUNT, ) await conn.commit() @@ -246,6 +284,7 @@ async def test_get_locks_history_respects_limit( await locks_store.insert( conn, scope="global", rule_name=f"R{i}", reason="x", locked_until=until, + account_id=TEST_ACCOUNT, ) await conn.commit() @@ -274,6 +313,7 @@ async def test_unlock_success( locked_until=until, market="binance", symbol="BTC/USDT@binance", + account_id=TEST_ACCOUNT, ) await conn.commit() diff --git a/services/paper/tests/test_evaluation_executor.py b/services/paper/tests/test_evaluation_executor.py new file mode 100644 index 00000000..7497ce5e --- /dev/null +++ b/services/paper/tests/test_evaluation_executor.py @@ -0,0 +1,119 @@ +"""一次性回测子进程执行器测试。""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime, timedelta +from itertools import pairwise + +import pytest + +from inalpha_paper.evaluation_executor import KillableEngineRunner, WorkerExecutionError +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.model.data import Bar +from inalpha_paper.strategy_evaluation import evaluate_buy_and_hold, evaluate_strategy_source + +_BAD_CONTRACT_SOURCE = """ +class TestStrategy(Strategy): + pass +""" + +_HANGING_CLASS_SOURCE = """ +class TestStrategy(Strategy): + while True: + pass +""" + + +def _bars() -> list[Bar]: + instrument = InstrumentId(symbol="BTCUSDT", venue="binance") + start = datetime(2026, 1, 1, tzinfo=UTC) + result: list[Bar] = [] + for index in range(20): + price = 100.0 + index + ts = int((start + timedelta(hours=index)).timestamp() * 1_000_000_000) + result.append( + Bar( + instrument_id=instrument, + timeframe="1h", + open=price, + high=price + 1.0, + low=price - 1.0, + close=price, + volume=10.0, + ts_event=ts, + ts_init=ts, + ) + ) + return result + + +@pytest.mark.asyncio +async def test_killable_runner_keeps_event_loop_responsive() -> None: + bars = _bars() + runner = KillableEngineRunner(timeout_s=10.0, mem_gb=2.0) + task = asyncio.create_task( + evaluate_buy_and_hold( + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=runner, + ) + ) + loop = asyncio.get_running_loop() + tick_times = [loop.time()] + while not task.done(): + await asyncio.sleep(0.01) + tick_times.append(loop.time()) + result = await task + gaps = [right - left for left, right in pairwise(tick_times)] + + assert gaps + assert max(gaps) < 0.5 + assert result.snapshot.num_bars == len(bars) + + +@pytest.mark.asyncio +async def test_killable_runner_enforces_wall_timeout() -> None: + bars = _bars() + runner = KillableEngineRunner(timeout_s=0.001, mem_gb=2.0) + + with pytest.raises(TimeoutError): + await evaluate_buy_and_hold( + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=runner, + ) + + +@pytest.mark.asyncio +async def test_contract_check_runs_inside_worker() -> None: + bars = _bars() + runner = KillableEngineRunner(timeout_s=5.0, mem_gb=2.0) + + with pytest.raises(WorkerExecutionError) as error: + await evaluate_strategy_source( + source_code=_BAD_CONTRACT_SOURCE, + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=runner, + ) + + assert error.value.code == "CANDIDATE_CONTRACT_FAILED" + + +@pytest.mark.asyncio +async def test_class_body_hang_is_killed_by_worker_timeout() -> None: + bars = _bars() + runner = KillableEngineRunner(timeout_s=0.2, mem_gb=2.0) + + with pytest.raises(TimeoutError): + await evaluate_strategy_source( + source_code=_HANGING_CLASS_SOURCE, + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=runner, + ) diff --git a/services/paper/tests/test_market_cutoff.py b/services/paper/tests/test_market_cutoff.py new file mode 100644 index 00000000..93c724b8 --- /dev/null +++ b/services/paper/tests/test_market_cutoff.py @@ -0,0 +1,109 @@ +"""市场已收盘 cutoff 边界测试。""" +from datetime import UTC, datetime + +from inalpha_paper.market_cutoff import expected_latest_bar_open +from inalpha_paper.market_evaluation import build_market_evaluation_context + + +def _context(venue: str, symbol: str, timeframe: str, as_of: datetime): + return build_market_evaluation_context( + venue=venue, + symbol=symbol, + timeframe=timeframe, + as_of=as_of, + ) + + +def test_crypto_cutoff_excludes_current_hour() -> None: + as_of = datetime(2026, 8, 12, 12, 34, tzinfo=UTC) + context = _context("binance", "BTCUSDT", "1h", as_of) + assert expected_latest_bar_open(context, as_of) == datetime( + 2026, 8, 12, 11, tzinfo=UTC + ) + + +def test_equity_daily_waits_for_session_close() -> None: + intraday = datetime(2026, 8, 12, 18, tzinfo=UTC) + context = _context("yfinance", "AAPL", "1d", intraday) + assert expected_latest_bar_open(context, intraday) == datetime( + 2026, 8, 11, 4, tzinfo=UTC + ) + + after_close = datetime(2026, 8, 12, 21, tzinfo=UTC) + assert expected_latest_bar_open(context, after_close) == datetime( + 2026, 8, 12, 4, tzinfo=UTC + ) + + +def test_equity_intraday_excludes_forming_interval() -> None: + as_of = datetime(2026, 8, 12, 15, 45, tzinfo=UTC) + context = _context("yfinance", "AAPL", "1h", as_of) + assert expected_latest_bar_open(context, as_of) == datetime( + 2026, 8, 12, 14, 30, tzinfo=UTC + ) + + +def test_a_share_intraday_respects_lunch_break() -> None: + as_of = datetime(2026, 8, 12, 5, 15, tzinfo=UTC) + context = _context("baostock", "sh.600519", "1h", as_of) + assert expected_latest_bar_open(context, as_of) == datetime( + 2026, 8, 12, 3, 30, tzinfo=UTC + ) + + +def test_us_hourly_grid_keeps_regular_and_half_day_short_tail() -> None: + regular = datetime(2026, 8, 12, 20, 1, tzinfo=UTC) + context = _context("yfinance", "AAPL", "1h", regular) + assert expected_latest_bar_open(context, regular) == datetime( + 2026, 8, 12, 19, 30, tzinfo=UTC + ) + + half_day = datetime(2026, 11, 27, 18, 1, tzinfo=UTC) + context = _context("yfinance", "AAPL", "1h", half_day) + assert expected_latest_bar_open(context, half_day) == datetime( + 2026, 11, 27, 17, 30, tzinfo=UTC + ) + + +def test_us_hourly_grid_tracks_dst() -> None: + before = datetime(2026, 3, 6, 15, 31, tzinfo=UTC) + after = datetime(2026, 3, 9, 14, 31, tzinfo=UTC) + context = _context("yfinance", "AAPL", "1h", before) + assert expected_latest_bar_open(context, before).hour == 14 + assert expected_latest_bar_open(context, after).hour == 13 + + +def test_break_markets_restart_grid_after_lunch() -> None: + hong_kong = datetime(2026, 8, 12, 6, 1, tzinfo=UTC) + hk_context = _context("yfinance", "0700.HK", "1h", hong_kong) + assert expected_latest_bar_open(hk_context, hong_kong) == datetime( + 2026, 8, 12, 5, tzinfo=UTC + ) + + tokyo = datetime(2026, 8, 12, 4, 31, tzinfo=UTC) + jp_context = _context("yfinance", "7203.T", "1h", tokyo) + assert expected_latest_bar_open(jp_context, tokyo) == datetime( + 2026, 8, 12, 3, 30, tzinfo=UTC + ) + + +def test_week_and_month_wait_for_actual_last_session_close() -> None: + before_week_close = datetime(2026, 8, 14, 6, 59, tzinfo=UTC) + after_week_close = datetime(2026, 8, 14, 7, 1, tzinfo=UTC) + weekly = _context("baostock", "sh.600519", "1wk", before_week_close) + assert expected_latest_bar_open(weekly, before_week_close) == datetime( + 2026, 8, 7, tzinfo=UTC + ) + assert expected_latest_bar_open(weekly, after_week_close) == datetime( + 2026, 8, 14, tzinfo=UTC + ) + + before_month_close = datetime(2026, 8, 31, 6, 59, tzinfo=UTC) + after_month_close = datetime(2026, 8, 31, 7, 1, tzinfo=UTC) + monthly = _context("baostock", "sh.600519", "1mo", before_month_close) + assert expected_latest_bar_open(monthly, before_month_close) == datetime( + 2026, 7, 31, tzinfo=UTC + ) + assert expected_latest_bar_open(monthly, after_month_close) == datetime( + 2026, 8, 31, tzinfo=UTC + ) diff --git a/services/paper/tests/test_market_evaluation.py b/services/paper/tests/test_market_evaluation.py new file mode 100644 index 00000000..dc95a416 --- /dev/null +++ b/services/paper/tests/test_market_evaluation.py @@ -0,0 +1,123 @@ +"""市场评估周期与年化契约测试。""" +from datetime import UTC, datetime + +import pytest +from inalpha_shared.errors import ValidationError + +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.market_evaluation import ( + build_market_evaluation_context, + canonical_timeframe, + fixed_timeframe_seconds, +) +from inalpha_paper.model.data import Bar +from inalpha_paper.runner import run_engine_in_subprocess + +_AS_OF = datetime(2026, 8, 11, 12, tzinfo=UTC) + + +def test_crypto_uses_24_7_annualization_and_alias() -> None: + context = build_market_evaluation_context( + venue="binance", + symbol="BTCUSDT", + timeframe="1wk", + as_of=_AS_OF, + ) + assert context.canonical_timeframe == "1w" + assert context.annualization_periods == 52 + assert context.calendar_code is None + + +def test_equity_uses_exchange_sessions() -> None: + context = build_market_evaluation_context( + venue="yfinance", + symbol="AAPL", + timeframe="1d", + as_of=_AS_OF, + ) + assert context.calendar_code == "XNYS" + assert 245 <= context.annualization_periods <= 255 + assert context.annualization_periods != 365 + + +def test_intraday_equity_uses_session_minutes() -> None: + context = build_market_evaluation_context( + venue="baostock", + symbol="sh.600519", + timeframe="1h", + as_of=_AS_OF, + ) + assert context.calendar_code == "XSHG" + assert 900 <= context.annualization_periods <= 1100 + + +def test_intraday_annualization_counts_short_tail_grid_bars() -> None: + context = build_market_evaluation_context( + venue="yfinance", + symbol="AAPL", + timeframe="1h", + as_of=_AS_OF, + ) + assert 1740 <= context.annualization_periods <= 1770 + + +def test_timeframe_helpers_are_explicit() -> None: + assert canonical_timeframe("1mo") == "1M" + assert fixed_timeframe_seconds("1wk") == 7 * 86_400 + assert fixed_timeframe_seconds("1mo") is None + + +@pytest.mark.parametrize( + ("venue", "symbol", "code"), + [ + ("fred", "GDP", "EVOLUTION_MARKET_UNSUPPORTED"), + ("unknown", "ABC", "EVOLUTION_MARKET_CALENDAR_UNKNOWN"), + ], +) +def test_non_tradable_or_unknown_market_fails_closed( + venue: str, + symbol: str, + code: str, +) -> None: + with pytest.raises(ValidationError) as error: + build_market_evaluation_context( + venue=venue, + symbol=symbol, + timeframe="1d", + as_of=_AS_OF, + ) + assert error.value.code == code + + +def test_engine_report_uses_explicit_annualization() -> None: + instrument = InstrumentId(symbol="TEST", venue="yfinance") + prices = [100.0, 102.0, 99.0, 104.0, 101.0] + bars = [ + Bar( + instrument_id=instrument, + timeframe="1d", + open=price, + high=price + 1.0, + low=price - 1.0, + close=price, + volume=100.0, + ts_event=(index + 1) * 86_400_000_000_000, + ts_init=(index + 1) * 86_400_000_000_000, + ) + for index, price in enumerate(prices) + ] + common = { + "bars": bars, + "instrument_id": instrument, + "timeframe": "1d", + "strategy_id": "buy_and_hold", + "params": {"trade_size": 1.0}, + "initial_cash": 10_000.0, + "fee_rate": 0.0, + } + report_252 = run_engine_in_subprocess(**common, annualization_periods=252) + report_365 = run_engine_in_subprocess(**common, annualization_periods=365) + + assert report_252.sharpe is not None + assert report_365.sharpe is not None + assert report_252.sharpe / report_365.sharpe == pytest.approx((252 / 365) ** 0.5) diff --git a/services/paper/tests/test_strategy_evaluation.py b/services/paper/tests/test_strategy_evaluation.py new file mode 100644 index 00000000..f65be246 --- /dev/null +++ b/services/paper/tests/test_strategy_evaluation.py @@ -0,0 +1,130 @@ +"""临时源码评估成功路径。""" + +from __future__ import annotations + +import asyncio +import json +import threading +import time +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest + +from inalpha_paper.engine.report import BacktestReport +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.model.data import Bar +from inalpha_paper.strategy_evaluation import evaluate_strategy_source + +_SOURCE = """ +class TestStrategy(Strategy): + def __init__(self, name, clock, msgbus, instrument_id, timeframe="1h"): + super().__init__(name, clock, msgbus) + self._instrument_id = instrument_id + + def on_bar(self, bar): + pass +""" + + +def _bars(count: int = 40) -> list[Bar]: + instrument = InstrumentId(symbol="BTC/USDT", venue="binance") + start = datetime(2026, 1, 1, tzinfo=UTC) + result: list[Bar] = [] + for index in range(count): + price = 100.0 + index + ts = int((start + timedelta(hours=index)).timestamp() * 1_000_000_000) + result.append( + Bar( + instrument_id=instrument, + timeframe="1h", + open=price, + high=price + 1.0, + low=price - 1.0, + close=price, + volume=10.0, + ts_event=ts, + ts_init=ts, + ) + ) + return result + + +def _report(bars: list[Bar]) -> BacktestReport: + curve = [(bar.ts_event, 10_000.0 + index * 10.0) for index, bar in enumerate(bars)] + return BacktestReport( + initial_cash=10_000.0, + final_equity=curve[-1][1], + total_return_pct=(curve[-1][1] / curve[0][1] - 1.0) * 100.0, + num_trades=6, + total_fees=1.5, + num_bars_processed=len(bars), + period_start=datetime.fromtimestamp(bars[0].ts_event / 1e9, tz=UTC), + period_end=datetime.fromtimestamp(bars[-1].ts_event / 1e9, tz=UTC), + positions={}, + sharpe=1.2, + sortino=1.5, + max_drawdown_pct=2.0, + equity_curve=curve, + ) + + +@pytest.mark.asyncio +async def test_source_evaluation_returns_json_snapshot() -> None: + bars = _bars() + calls: list[dict[str, Any]] = [] + + async def run_engine(**kwargs: Any) -> BacktestReport: + calls.append(kwargs) + return _report(kwargs["bars"]) + + result = await evaluate_strategy_source( + source_code=_SOURCE, + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=run_engine, + annualization_periods=8760.0, + ) + + assert calls[0]["bars"] is bars + assert calls[0]["candidate_code"] == _SOURCE + assert result.snapshot.fitness > 0 + assert result.snapshot.validation is not None + assert "equity_curve" not in result.snapshot.model_dump() + json.dumps(result.snapshot.model_dump(mode="json")) + + +@pytest.mark.asyncio +async def test_validation_metrics_run_off_event_loop( + monkeypatch: pytest.MonkeyPatch, +) -> None: + bars = _bars() + main_thread = threading.get_ident() + worker_threads: list[int] = [] + + async def run_engine(**kwargs: Any) -> BacktestReport: + return _report(kwargs["bars"]) + + def slow_validation(*_args: Any, **_kwargs: Any) -> None: + worker_threads.append(threading.get_ident()) + time.sleep(0.1) + + monkeypatch.setattr("inalpha_paper.strategy_evaluation.validation_from_report", slow_validation) + task = asyncio.create_task( + evaluate_strategy_source( + source_code=_SOURCE, + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=run_engine, + ) + ) + ticks = 0 + while not task.done(): + ticks += 1 + await asyncio.sleep(0.01) + await task + + assert ticks > 2 + assert worker_threads and worker_threads[0] != main_thread diff --git a/services/paper/tests/test_strategy_evaluation_errors.py b/services/paper/tests/test_strategy_evaluation_errors.py new file mode 100644 index 00000000..4bfaa4a6 --- /dev/null +++ b/services/paper/tests/test_strategy_evaluation_errors.py @@ -0,0 +1,93 @@ +"""临时源码评估拒绝路径。""" +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest +from inalpha_shared.errors import ValidationError + +from inalpha_paper.engine.report import BacktestReport +from inalpha_paper.kernel.identifiers import InstrumentId +from inalpha_paper.model.data import Bar +from inalpha_paper.strategy_evaluation import ( + evaluate_buy_and_hold, + evaluate_strategy_source, +) + +_SOURCE = """ +class TestStrategy(Strategy): + def __init__(self, name, clock, msgbus, instrument_id, timeframe="1h"): + super().__init__(name, clock, msgbus) + + def on_bar(self, bar): + pass +""" + + +def _bars(count: int = 2, *, second_open: float = 101.0) -> list[Bar]: + instrument = InstrumentId(symbol="BTC/USDT", venue="binance") + start = datetime(2026, 1, 1, tzinfo=UTC) + result: list[Bar] = [] + for index in range(count): + price = second_open if index == 1 else 100.0 + ts = int((start + timedelta(hours=index)).timestamp() * 1_000_000_000) + result.append( + Bar( + instrument_id=instrument, + timeframe="1h", + open=price, + high=max(price, 101.0), + low=min(price, 99.0), + close=100.0, + volume=10.0, + ts_event=ts, + ts_init=ts, + ) + ) + return result + + +async def _unreachable(**_kwargs: Any) -> BacktestReport: + raise AssertionError("unreachable") + + +@pytest.mark.asyncio +async def test_invalid_source_is_rejected_before_engine() -> None: + bars = _bars() + with pytest.raises(ValidationError) as error: + await evaluate_strategy_source( + source_code="import os", + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=_unreachable, + ) + assert error.value.code == "CANDIDATE_REAUDIT_FAILED" + + +@pytest.mark.asyncio +async def test_baseline_validates_price_before_engine() -> None: + bars = _bars(second_open=0.0) + with pytest.raises(ValidationError) as error: + await evaluate_buy_and_hold( + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=_unreachable, + ) + assert error.value.code == "INVALID_BAR_PRICE" + + +@pytest.mark.asyncio +async def test_evaluation_requires_two_bars() -> None: + bars = _bars(1) + with pytest.raises(ValidationError) as error: + await evaluate_strategy_source( + source_code=_SOURCE, + bars=bars, + instrument_id=bars[0].instrument_id, + timeframe="1h", + run_engine=_unreachable, + ) + assert error.value.code == "NO_BARS_AVAILABLE"