diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 56391dbe..1157b8f1 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,5 +1,7 @@ #!/usr/bin/env bash -# Pre-commit gate: make all (format BE+FE + lint + typecheck + test) + dashboard build. +# Pre-commit gate: make precommit (format BE+FE + lint + typecheck + testmon-scoped +# tests) + dashboard build. The full suite still runs in CI (`make all`); this is a +# fast, change-aware local gate so commits don't wait on the whole test matrix. # Enable once per clone: make install-hooks # Bypass (emergency only): SKIP_PRECOMMIT=1 git commit ... # Or: git commit --no-verify @@ -20,8 +22,8 @@ while IFS= read -r line; do [[ -n "$line" ]] && STAGED_FILES+=("$line") done < <(git diff --cached --name-only --diff-filter=ACMR || true) -echo "[pre-commit] make all (format-all + lint + typecheck + test)" -make all +echo "[pre-commit] make precommit (format-all + lint + typecheck + testmon-scoped test)" +make precommit # If format rewrote files that were already staged, refresh the index so the # commit includes the formatted content (worktree vs index drift). diff --git a/.github/workflows/anti-spam-issues.yml b/.github/workflows/anti-spam-issues.yml new file mode 100644 index 00000000..48155a2b --- /dev/null +++ b/.github/workflows/anti-spam-issues.yml @@ -0,0 +1,303 @@ +name: Anti-spam issue guard + +"on": + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to check manually, for example 19 or #19" + required: true + type: string + +permissions: + contents: read + issues: write + +jobs: + anti-spam: + runs-on: ubuntu-latest + + steps: + - name: Close suspicious issues + uses: actions/github-script@v7 + env: + LABEL_NAME: "pending-maintainer-review" + LABEL_COLOR: "cfd3d7" + AUTO_CLOSE: "true" + NEW_ACCOUNT_DAYS: "180" + + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + let issue = context.payload.issue; + + if (context.eventName === "workflow_dispatch") { + const rawIssueNumber = ( + core.getInput("issue_number") || + context.payload.inputs?.issue_number || + "" + ).trim(); + + core.info(`Manual issue_number raw input: "${rawIssueNumber}"`); + core.info(`Workflow inputs payload: ${JSON.stringify(context.payload.inputs || {})}`); + + const issueNumber = Number(rawIssueNumber.replace(/^#/, "")); + + if (!Number.isInteger(issueNumber) || issueNumber <= 0) { + core.setFailed(`Invalid issue_number: "${rawIssueNumber}"`); + return; + } + + const issueResponse = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber + }); + + issue = issueResponse.data; + } + + if (!issue || issue.pull_request) { + core.info("Not a normal issue, skipping."); + return; + } + + const author = issue.user.login; + const association = issue.author_association || "NONE"; + + const trustedAssociations = new Set([ + "OWNER", + "MEMBER", + "COLLABORATOR", + "CONTRIBUTOR" + ]); + + if (trustedAssociations.has(association)) { + core.info(`Trusted author association: ${association}. Skipping.`); + return; + } + + if (issue.user.type === "Bot") { + core.info("Issue author is a bot. Skipping."); + return; + } + + const userInfo = await github.rest.users.getByUsername({ + username: author + }); + + const createdAt = new Date(userInfo.data.created_at); + const accountAgeDays = Math.floor((Date.now() - createdAt.getTime()) / 86400000); + const newAccountDays = Number(process.env.NEW_ACCOUNT_DAYS || 180); + const isNewAccount = accountAgeDays <= newAccountDays; + + const issueText = `${issue.title || ""}\n${issue.body || ""}`; + const normalizedIssueText = issueText.replace(/\s+/g, " "); + + function hit(patterns) { + return patterns.some(pattern => pattern.test(normalizedIssueText)); + } + + const strongStarFarmPatterns = [ + /刷\s*星|买\s*星|假\s*星|增\s*星|刷\s*star|假\s*star/i, + /star\s*造假|star\s*作弊|star\s*作假|star\s*异常/i, + /star\s*farming|artificial\s*stars?|fake\s*stars?|bot\s*stars?/i, + /star\s*boost|star\s*boosting|boosting\s*service/i, + /bought\s*stars?|purchased\s*stars?|paid\s*stars?/i + ]; + + const starContextPatterns = [ + /\bstars?\b|starred|stargazers?/i, + /Star\s*数|Star\s*数量|Star\s*数据|Star\s*列表/i, + /星标|颗星|个星|这么多\s*Star/i, + /人气|热度/ + ]; + + const abnormalGrowthPatterns = [ + /异常增长|突然增长|爆发式增长|快速增长|增长异常/i, + /暴增|飙升|猛增|激增|疯涨|刷到|涨到/i, + /过去\s*48\s*小时|48\s*小时|仅用了?\s*\d+\s*天|两天|2\s*天/i, + /新增了?\s*\d+|增加了?\s*\d+|从\s*\d+\s*飙升到\s*\d+/i, + /\d+\s*(颗|个)?\s*(Star|stars?|星)/i, + /创建时间集中|创建时间高度集中|注册时间高度集中/i + ]; + + const suspiciousAccountPatterns = [ + /注册日期集中|注册时间集中|创建日期集中|创建时间集中/i, + /集中在最近|最近\s*(两|2)\s*周|最近\s*\d+\s*天|7\s*天内|一周内/i, + /用户名像随机|随机字符串|随机生成/i, + /个人主页空空如也|主页空空如也|主页空|个人主页空/i, + /没有头像|无头像|没有仓库|无公开仓库|没有公开仓库/i, + /没有\s*followers?|没有\s*follower|无粉丝|零粉丝|no\s*followers?/i, + /空壳号|僵尸号|机器人账号|bot\s*accounts?/i, + /只\s*Star|只\s*star\s*不超过|Stargazer\s*列表|抽查了?\s*\d+\s*个账号/i + ]; + + const reportThreatPatterns = [ + /违规|举报|上报|报告给\s*GitHub|发往\s*GitHub/i, + /违规举报|举报到位|举报已递|举报已提交|举报交官方/i, + /违规流量|违规人气|清理人气|清除流量|流量清除/i, + /清零|清空|封禁|封禁项目|关闭项目|项目关停|项目关闭|项目下线/i, + /关停下线|入口关闭|归档备查|违规已定/i + ]; + + const metricMismatchPatterns = [ + /(npm|pip|下载量).{0,80}(Star|stars?|星)/i, + /(Star|stars?|星).{0,80}(npm|pip|下载量)/i, + /(Issue|PR|Fork|贡献者|社区活跃|活跃指标|月均).{0,80}(不匹配|太低|很低|对比|同类项目)/i, + /(同类项目|类似项目|对比关键指标|关键指标|指标).{0,80}(Star|stars?|星)/i, + /只有\s*Star\s*高|其他指标都远低于同类/i, + /真实用户|真实反馈|真实内容|真实评价/i + ]; + + const reputationAttackPatterns = [ + /诚信问题|诚信有问题|信任崩塌|失望|反面教材/i, + /简历|面试|候选人|找工作|pass\s*的|直接\s*pass/i, + /技术分享|教学案例|识别刷星项目/i, + /真实口碑|虚假热度|虚假的数字|真实用户的认可/i + ]; + + const strongKeywordHit = hit(strongStarFarmPatterns); + const hasStarContext = hit(starContextPatterns); + const abnormalGrowthHit = hit(abnormalGrowthPatterns); + const suspiciousAccountHit = hit(suspiciousAccountPatterns); + const reportThreatHit = hit(reportThreatPatterns); + const metricMismatchHit = hit(metricMismatchPatterns); + const reputationAttackHit = hit(reputationAttackPatterns); + + const starAccusationScore = [ + abnormalGrowthHit, + suspiciousAccountHit, + reportThreatHit, + metricMismatchHit, + reputationAttackHit + ].filter(Boolean).length; + + const coordinatedThreatWithoutStarHit = + suspiciousAccountHit && + reportThreatHit && + /注册日期集中|注册时间集中|创建时间集中|最近\s*(两|2)\s*周|随机字符串|空壳号|无公开仓库|没有仓库|无粉丝|没有\s*followers?/i.test(normalizedIssueText); + + const keywordHit = + strongKeywordHit || + (hasStarContext && starAccusationScore >= 2) || + coordinatedThreatWithoutStarHit; + + let priorActivity = false; + + try { + const authoredSearch = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} author:${author}`, + per_page: 20 + }); + + const priorAuthoredItems = authoredSearch.data.items.filter( + item => item.number !== issue.number + ); + + if (priorAuthoredItems.length > 0) { + priorActivity = true; + } + } catch (error) { + core.warning(`Failed to search authored issues/PRs: ${error.message}`); + } + + try { + const commentedSearch = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} commenter:${author}`, + per_page: 20 + }); + + const priorCommentedItems = commentedSearch.data.items.filter( + item => item.number !== issue.number + ); + + if (priorCommentedItems.length > 0) { + priorActivity = true; + } + } catch (error) { + core.warning(`Failed to search commented issues/PRs: ${error.message}`); + } + + const suspicious = + priorActivity === false && + isNewAccount === true && + keywordHit === true; + + core.info(`Issue number: ${issue.number}`); + core.info(`Issue title: ${issue.title}`); + core.info(`Author: ${author}`); + core.info(`Association: ${association}`); + core.info(`Account age days: ${accountAgeDays}`); + core.info(`Is new account within ${newAccountDays} days: ${isNewAccount}`); + core.info(`Prior activity in repo: ${priorActivity}`); + core.info(`Strong keyword hit: ${strongKeywordHit}`); + core.info(`Has star context: ${hasStarContext}`); + core.info(`Abnormal growth hit: ${abnormalGrowthHit}`); + core.info(`Suspicious account hit: ${suspiciousAccountHit}`); + core.info(`Report/threat hit: ${reportThreatHit}`); + core.info(`Metric mismatch hit: ${metricMismatchHit}`); + core.info(`Reputation attack hit: ${reputationAttackHit}`); + core.info(`Star accusation score: ${starAccusationScore}`); + core.info(`Coordinated threat without star hit: ${coordinatedThreatWithoutStarHit}`); + core.info(`Final keyword hit: ${keywordHit}`); + core.info(`Suspicious: ${suspicious}`); + + if (!suspicious) { + return; + } + + const labelName = process.env.LABEL_NAME || "pending-maintainer-review"; + const labelColor = process.env.LABEL_COLOR || "cfd3d7"; + const autoClose = process.env.AUTO_CLOSE === "true"; + + try { + await github.rest.issues.createLabel({ + owner, + repo, + name: labelName, + color: labelColor, + description: "Automatically closed by anti-spam guard; pending maintainer review" + }); + } catch (error) { + if (error.status !== 422) { + core.warning(`Failed to create label: ${error.message}`); + } + } + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issue.number, + labels: [labelName] + }); + + const commentBody = [ + "👋 This issue was automatically closed by an anti-spam guard because it was opened by a recently created GitHub account with **no prior activity in this repository**, and the issue content matches a recent pattern of repetitive star-farming accusations.", + "", + "If you are a real user with a genuine report, sorry for the friction — just **leave a comment below** explaining your situation and a maintainer will reopen it.", + "", + "---", + "", + "本 Issue 由反刷屏自动规则关闭:提交账号为近期注册账号,且**从未与本仓库互动**,同时 Issue 内容匹配近期重复出现的刷星举报模式。如果你是真实用户,请在下方留言说明,维护者会重新开启。" + ].join("\n"); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: commentBody + }); + + if (autoClose) { + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + state: "closed", + state_reason: "not_planned" + }); + } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9589c41f..8a198ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ ENV/ # Test / coverage .pytest_cache/ +.testmondata* .mypy_cache/ .ruff_cache/ .coverage @@ -90,3 +91,4 @@ scripts/dev-local-link.sh # Local agent planning docs (not for upstream) docs/superpowers/ scripts/lh_image_install_octop_tencentos.sh +.worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index eacf11a1..5f96ff10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,44 @@ ## [Unreleased] +## [0.9.24] - 2026-08-15 + +### 新增 +- 知识库:新增知识库与对话检索,支持本地 ONNX 向量嵌入模型运行 +- 认证:新增 OpenID Connect(OIDC)单点登录 +- 权限:新增按用户模块权限(RBAC)及管理员绕过 +- 专家:支持将工作区快照发布为可安装模板(专家市场) +- 智能体:支持将智能体共享给其他用户 +- 技能:新增对话式技能管理器(SkillHub),并兼容 Windows +- 备份:新增自动定时系统备份 +- 频道:新增 final-only 仅终稿回复模式 +- 线程:支持会话线程分叉(fork) +- 体验:HITL 工具选择器、运行时按需安装、KB/技能 UX 优化;对话与镜像等界面打磨;知识嵌入初始化流程加固 + +### 修复 +- 媒体/预览白名单补充音频 MIME 类型 +- 修正 ONNX 下载检测在未安装 fastembed 时的误判 +- 加固更新状态缓存与存储处理 +- 预提交门控:修复 staged 变更检测,避免 testmon 门控误报为绿 + +### 变更 +- 升级 harness-browser 依赖至 0.7.5 +- 数据库 schema 收敛为 v5 +- 备份恢复面板图标更新为 CalendarClock;对话/镜像等界面打磨 + ## [0.9.23] - 2026-08-13 ### 修复 - 取消首次引导时删除 `octop-login.txt` 引导密码文件的逻辑,避免引导密码意外丢失 - 修复安装脚本版本显示问题,并将安装输出调整为英文 +### 新增 + +- Octop-owned built-in `skill-manager` for conversational Skill lifecycle + management from uploaded files, archives, Git/GitHub or web URLs, and + SkillHub. It is seeded into every agent instance without modifying + harness-agent and installs user Skills only under that agent's `skills/`. + ## [0.9.22] - 2026-08-11 ### 新增 diff --git a/Makefile b/Makefile index 6a72cdd4..37ac43ee 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,7 @@ help: @echo "" @echo "Quality targets (ship bar):" @echo " all format-all + lint + typecheck + test (backend lint/typecheck/test)" + @echo " precommit fast change-aware gate for the git pre-commit hook (testmon-scoped tests)" @echo " lint Ruff check + format check (src, tests)" @echo " format Ruff auto-fix + format (src, tests)" @echo " typecheck mypy --strict src/octop" @@ -226,6 +227,29 @@ test-live: @echo "[test] pytest (live)..." $(RUN) pytest -m live +# Fast, change-aware gate for the local pre-commit hook. Runs ruff/mypy (cheap +# and kept full for accuracy) plus only the tests affected by changed files via +# pytest-testmon. The full suite still runs in CI (`make all`); this is local +# feedback only, so a missed cross-module impact is caught there. +.PHONY: precommit +precommit: format-all lint typecheck test-affected + +# NOTE: do NOT pass `-m` here — pytest-testmon deactivates its affected-test +# selection whenever a marker expression is present, which would fall back to +# the full suite. Live tests under tests/live/ auto-skip when credentials are +# absent, so running them unscoped is cheap and safe; the CI `make all` gate +# keeps `-m "not live"` for the authoritative full run. +# +# NOTE: testmon detects changes via `git ls-files -m` (worktree vs index), which +# is empty once changes are staged — exactly the pre-commit state — so it would +# silently run zero tests. The patch in conftest.py / tests/support/ +# testmon_staged_changes.py redirects detection to `git diff HEAD` (staged + +# unstaged) so the gate actually fires on a commit's changes. +.PHONY: test-affected +test-affected: + @echo "[test] pytest (testmon: only tests affected by changes)..." + $(RUN) pytest --testmon + # ─── Quality (frontend) ────────────────────────────────────────────────────── .PHONY: lint-frontend @@ -266,7 +290,7 @@ install-hooks: @echo "[install-hooks] Setting core.hooksPath=.githooks" git config core.hooksPath .githooks @chmod +x "$(REPO_ROOT)/.githooks/"* 2>/dev/null || true - @echo "[install-hooks] Done. Pre-commit will run: make all (incl. format-all), dashboard build" + @echo "[install-hooks] Done. Pre-commit will run: make precommit (format-all + lint + typecheck + testmon-scoped tests), dashboard build" @echo "[install-hooks] Bypass: SKIP_PRECOMMIT=1 git commit … or git commit --no-verify" .PHONY: install install-dev diff --git a/README.md b/README.md index 38595c30..f39a2a8a 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@

Python 3.12+ License: MIT - Version + Version PyPI Code Style: Ruff GitHub stars + GitHub forks + Discord

@@ -186,6 +188,14 @@ See [scripts/README.md](scripts/README.md) for all install options (`--version`, ```bash pip install octop # optional: pip install "octop[browser]" +# optional local ONNX embedding model cache (Models → Local): pip install "octop[local-embedding]" +# Downloads catalog weights under ~/.octop/embedding_models; not chat, not Memory. +``` + +From a source checkout with uv: + +```bash +uv sync --extra local-embedding ``` ### 2. Initialize diff --git a/README_CN.md b/README_CN.md index 525c1f35..9fc11fa3 100644 --- a/README_CN.md +++ b/README_CN.md @@ -12,6 +12,8 @@ PyPI Code Style: Ruff GitHub stars + GitHub forks + Discord

@@ -197,6 +199,14 @@ curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install ```bash pip install octop # 可选:pip install "octop[browser]" +# 可选本地 ONNX Embedding 模型缓存(设置 → 模型 → 本地):pip install "octop[local-embedding]" +# 仅下载目录模型到 ~/.octop/embedding_models;不用于对话,也不接入 Memory。 +``` + +从源码用 uv 开发时: + +```bash +uv sync --extra local-embedding ``` ### 2. 启动 diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..5257fdd8 --- /dev/null +++ b/conftest.py @@ -0,0 +1,11 @@ +"""Repo-root conftest. + +Imported by pytest at startup (before any plugin's ``pytest_configure``), so the +testmon change-detection patch in ``tests.support.testmon_staged_changes`` is +applied before testmon reads file fingerprints. The patch only affects +testmon's internals and is a safe no-op when testmon is absent. +""" + +from __future__ import annotations + +import tests.support.testmon_staged_changes # noqa: F401 (applies the patch) diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 62b6544a..76614d63 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -5,6 +5,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import { useTranslation } from "react-i18next"; import MainLayout from "./layouts/MainLayout"; import LoginPage from "./pages/Login"; +import OidcComplete from "./pages/Login/OidcComplete"; import SetupPage from "./pages/Setup"; import AuthGuard from "./components/AuthGuard"; import { AntdAppProvider } from "./components/AntdAppProvider"; @@ -99,6 +100,7 @@ function ThemedApp() { } /> + } /> } /> request("/auth/oidc/status"), + + /** Start an OIDC authorization-code login flow. */ + startOidc: (redirect_after?: string) => + request<{ authorization_url: string }>("/auth/oidc/start", { + method: "POST", + body: JSON.stringify({ redirect_after }), + }), + + /** Exchange the one-time browser code for the standard JWT login response. */ + exchangeOidcCode: async (code: string): Promise => { + const raw = await request("/auth/oidc/exchange", { + method: "POST", + body: JSON.stringify({ code }), + }); + return { ...raw, token: raw.access_token }; + }, + /** Server-side logout (best-effort; client clears token regardless). */ logout: () => request("/auth/logout", { method: "POST" }).catch(() => undefined), diff --git a/dashboard/src/api/modules/backup.ts b/dashboard/src/api/modules/backup.ts index 3861b9df..712c8a01 100644 --- a/dashboard/src/api/modules/backup.ts +++ b/dashboard/src/api/modules/backup.ts @@ -12,9 +12,33 @@ export interface BackupListResponse { items: BackupFileItem[]; } +export interface AutoBackupSettings { + auto_enabled: boolean; + schedule: string; + retention_count: number; + scheduled?: boolean; +} + export const backupApi = { listBackups: () => request("/admin/backup/list"), + getAutoSettings: () => request("/admin/backup/auto"), + + updateAutoSettings: (body: { + auto_enabled: boolean; + schedule: string; + retention_count: number; + }) => + request<{ ok: boolean } & AutoBackupSettings>("/admin/backup/auto", { + method: "PUT", + body: JSON.stringify(body), + }), + + runAutoBackup: () => + request<{ ok: boolean; item: BackupFileItem }>("/admin/backup/auto/run", { + method: "POST", + }), + createBackup: () => request<{ ok: boolean; item: BackupFileItem }>("/admin/backup/create", { method: "POST", diff --git a/dashboard/src/api/modules/expertMarket.ts b/dashboard/src/api/modules/expertMarket.ts index 61be8555..8e9594a8 100644 --- a/dashboard/src/api/modules/expertMarket.ts +++ b/dashboard/src/api/modules/expertMarket.ts @@ -61,6 +61,13 @@ export interface CreateMarketExpertBody { providers?: string[]; default_model?: string; backend?: Record; + skill_package_ids?: string[]; + color?: string; + max_iters?: number | null; + max_input_length?: number | null; + temperature?: number | null; + top_p?: number | null; + max_tokens?: number | null; } function hubListPath(query: string, scene: string): string { diff --git a/dashboard/src/api/modules/knowledgeBases.test.ts b/dashboard/src/api/modules/knowledgeBases.test.ts new file mode 100644 index 00000000..9c3b9ec9 --- /dev/null +++ b/dashboard/src/api/modules/knowledgeBases.test.ts @@ -0,0 +1,87 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { request, requestUpload } = vi.hoisted(() => ({ + request: vi.fn(), + requestUpload: vi.fn(), +})); + +vi.mock("../request", () => ({ request, requestUpload })); + +import { knowledgeBasesApi } from "./knowledgeBases"; + +beforeEach(() => { + request.mockClear(); + requestUpload.mockClear(); +}); + +describe("knowledgeBasesApi", () => { + it("uses the knowledge-base capability and feature endpoints", () => { + knowledgeBasesApi.getCapability(); + knowledgeBasesApi.setFeature({ enabled: true, model: "BAAI/bge-small" }); + knowledgeBasesApi.downloadOnnx("BAAI/bge-small-zh-v1.5"); + knowledgeBasesApi.getOnnxDownloadStatus(); + knowledgeBasesApi.activateOnnx("BAAI/bge-small-zh-v1.5"); + + expect(request).toHaveBeenNthCalledWith(1, "/knowledge-bases/capability"); + expect(request).toHaveBeenNthCalledWith(2, "/knowledge-bases/feature", { + method: "PUT", + body: JSON.stringify({ enabled: true, model: "BAAI/bge-small" }), + }); + expect(request).toHaveBeenNthCalledWith( + 3, + "/knowledge-bases/onnx-download", + { + method: "POST", + body: JSON.stringify({ model: "BAAI/bge-small-zh-v1.5" }), + }, + ); + expect(request).toHaveBeenNthCalledWith( + 4, + "/knowledge-bases/onnx-download-status", + ); + expect(request).toHaveBeenNthCalledWith( + 5, + "/knowledge-bases/onnx-activate", + { + method: "POST", + body: JSON.stringify({ model: "BAAI/bge-small-zh-v1.5" }), + }, + ); + }); + + it("requests the full ONNX catalog when expanding embedding options", () => { + knowledgeBasesApi.getEmbeddingOptions(); + knowledgeBasesApi.getEmbeddingOptions({ allOnnx: true }); + + expect(request).toHaveBeenNthCalledWith( + 1, + "/knowledge-bases/embedding-options", + ); + expect(request).toHaveBeenNthCalledWith( + 2, + "/knowledge-bases/embedding-options?all_onnx=true", + ); + }); + + it("uses nested document endpoints", () => { + const file = new File(["document"], "notes.md", { type: "text/markdown" }); + knowledgeBasesApi.uploadDocument("kb-1", file); + knowledgeBasesApi.deleteDocument("kb-1", "doc-1"); + knowledgeBasesApi.previewDocument("kb-1", "doc-1"); + + expect(requestUpload).toHaveBeenCalledWith( + "/knowledge-bases/kb-1/documents", + expect.any(FormData), + { method: "POST" }, + ); + expect(request).toHaveBeenNthCalledWith( + 1, + "/knowledge-bases/kb-1/documents/doc-1", + { method: "DELETE" }, + ); + expect(request).toHaveBeenNthCalledWith( + 2, + "/knowledge-bases/kb-1/documents/doc-1/preview", + ); + }); +}); diff --git a/dashboard/src/api/modules/knowledgeBases.ts b/dashboard/src/api/modules/knowledgeBases.ts new file mode 100644 index 00000000..ddf4d6fe --- /dev/null +++ b/dashboard/src/api/modules/knowledgeBases.ts @@ -0,0 +1,195 @@ +import { request, requestUpload } from "../request"; + +export interface KnowledgeLimits { + max_bases_per_owner: number; + max_docs_per_kb: number; + max_document_bytes: number; +} + +export interface KnowledgeCapability { + feature_enabled: boolean; + backend: "onnx" | "remote"; + selected_model: string; + provider_id: string; + prerequisites_ok: boolean; + usable: boolean; + checks: { + model_selected: boolean; + model_downloaded: boolean; + deps_available: boolean; + provider_ready: boolean; + }; + limits?: KnowledgeLimits; +} + +export interface KnowledgeBase { + id: string; + owner_user_id: number; + owner_username?: string | null; + owner_display_name?: string | null; + name: string; + description: string; + default_open: boolean; + shared: boolean; + icon_name: string; + embedding_model: string; + embedding_dim: number; + doc_count: number; + created_at: number; + updated_at: number; +} + +export interface KnowledgeDocument { + id: string; + kb_id: string; + filename: string; + content_type: string; + byte_size: number; + content_hash: string; + status: "pending" | "processing" | "ready" | "failed"; + error_message: string; + chunk_count: number; + created_at: number; + updated_at: number; +} + +export interface KnowledgeOnnxModel { + id: string; + name: string; + downloaded: boolean; + recommended?: boolean; + size_gb?: number | null; +} + +export interface KnowledgeEmbeddingOptions { + onnx: KnowledgeOnnxModel[]; + remote: { + provider_id: string; + provider_name: string; + models: { id: string; name: string }[]; + }[]; +} + +export interface KnowledgeOnnxDownloadState { + status: "idle" | "downloading" | "loading" | "done" | "failed"; + progress: number; + error?: string | null; + model_name: string; + task_id?: string; +} + +export const DEFAULT_KNOWLEDGE_LIMITS: KnowledgeLimits = { + max_bases_per_owner: 20, + max_docs_per_kb: 100, + max_document_bytes: 20 * 1024 * 1024, +}; + +export const knowledgeBasesApi = { + getCapability: () => + request("/knowledge-bases/capability"), + + setFeature: (body: { + enabled: boolean; + backend?: "onnx" | "remote"; + model?: string; + provider_id?: string; + }) => + request("/knowledge-bases/feature", { + method: "PUT", + body: JSON.stringify(body), + }), + + list: () => request("/knowledge-bases"), + getEmbeddingOptions: (opts?: { allOnnx?: boolean }) => + request( + opts?.allOnnx + ? "/knowledge-bases/embedding-options?all_onnx=true" + : "/knowledge-bases/embedding-options", + ), + + downloadOnnx: (model: string) => + request("/knowledge-bases/onnx-download", { + method: "POST", + body: JSON.stringify({ model }), + }), + + getOnnxDownloadStatus: () => + request( + "/knowledge-bases/onnx-download-status", + ), + + activateOnnx: (model: string) => + request<{ enabled: boolean; model: string; ready: boolean }>( + "/knowledge-bases/onnx-activate", + { + method: "POST", + body: JSON.stringify({ model }), + }, + ), + + get: (id: string) => request(`/knowledge-bases/${id}`), + + create: (body: { + name: string; + description?: string; + default_open?: boolean; + shared?: boolean; + icon_name?: string; + }) => + request("/knowledge-bases", { + method: "POST", + body: JSON.stringify(body), + }), + + update: ( + id: string, + body: { + name?: string; + description?: string; + default_open?: boolean; + shared?: boolean; + icon_name?: string; + }, + ) => + request(`/knowledge-bases/${id}`, { + method: "PATCH", + body: JSON.stringify(body), + }), + + delete: (id: string) => + request(`/knowledge-bases/${id}`, { method: "DELETE" }), + + listDocuments: (id: string) => + request(`/knowledge-bases/${id}/documents`), + + uploadDocument: (id: string, file: File) => { + const body = new FormData(); + body.append("upload", file); + return requestUpload( + `/knowledge-bases/${id}/documents`, + body, + { method: "POST" }, + ); + }, + + deleteDocument: (id: string, documentId: string) => + request(`/knowledge-bases/${id}/documents/${documentId}`, { + method: "DELETE", + }), + + reindexDocument: (id: string, documentId: string) => + request( + `/knowledge-bases/${id}/documents/${documentId}/reindex`, + { method: "POST" }, + ), + + previewDocument: (id: string, documentId: string) => + request<{ id: string; filename: string; text: string }>( + `/knowledge-bases/${id}/documents/${documentId}/preview`, + ), + + reindex: (id: string) => + request<{ enqueued: number }>(`/knowledge-bases/${id}/reindex`, { + method: "POST", + }), +}; diff --git a/dashboard/src/api/modules/octopThreads.ts b/dashboard/src/api/modules/octopThreads.ts index b14ff4f9..98eacf31 100644 --- a/dashboard/src/api/modules/octopThreads.ts +++ b/dashboard/src/api/modules/octopThreads.ts @@ -152,6 +152,30 @@ export const octopThreadsApi = { { method: "DELETE" }, ), + fork: ( + agentId: string, + threadId: string, + body: { + message_id: string; + content?: string; + user_turns_from_end?: number; + }, + ) => + request<{ + thread_id: string; + session_key: string; + source_thread_id: string; + copied_messages: number; + title: string | null; + last_active: number; + created_at: number; + }>( + `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent( + threadId, + )}/fork`, + { method: "POST", body: JSON.stringify(body) }, + ), + markRead: (agentId: string, threadId: string) => request( `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent( diff --git a/dashboard/src/api/modules/ollamaModel.ts b/dashboard/src/api/modules/ollamaModel.ts index a29d5851..4d330b53 100644 --- a/dashboard/src/api/modules/ollamaModel.ts +++ b/dashboard/src/api/modules/ollamaModel.ts @@ -28,4 +28,13 @@ export const ollamaModelApi = { `/ollama-models/${encodeURIComponent(name)}`, { method: "DELETE" }, ), + + getService: () => + request<{ enabled: boolean; running: boolean }>("/ollama-models/service"), + + setService: (enabled: boolean) => + request<{ enabled: boolean; running: boolean }>("/ollama-models/service", { + method: "PUT", + body: JSON.stringify({ enabled }), + }), }; diff --git a/dashboard/src/api/modules/onnxDownloadWatcher.ts b/dashboard/src/api/modules/onnxDownloadWatcher.ts new file mode 100644 index 00000000..aba5196d --- /dev/null +++ b/dashboard/src/api/modules/onnxDownloadWatcher.ts @@ -0,0 +1,116 @@ +/** + * Background watcher for ONNX model downloads. + * Survives closing the progress / config modal; notifies once on completion. + */ +import { onnxModelApi, type OnnxDownloadState } from "./onnxModel"; + +export type OnnxDownloadProgressHandler = (state: OnnxDownloadState) => void; +export type OnnxDownloadTerminalHandler = ( + state: OnnxDownloadState, +) => void | Promise; + +interface WatchOptions { + modelId: string; + onProgress?: OnnxDownloadProgressHandler; + onTerminal: OnnxDownloadTerminalHandler; + intervalMs?: number; + maxTicks?: number; +} + +interface ActiveWatch { + modelId: string; + timer: ReturnType; + onProgress?: OnnxDownloadProgressHandler; + onTerminal: OnnxDownloadTerminalHandler; + ticks: number; + maxTicks: number; + finished: boolean; +} + +let active: ActiveWatch | null = null; + +export function isWatchingOnnxDownload(modelId?: string): boolean { + if (!active || active.finished) return false; + if (modelId) return active.modelId === modelId; + return true; +} + +export function getActiveOnnxDownloadModelId(): string | null { + return active && !active.finished ? active.modelId : null; +} + +/** Attach/replace UI progress listener without restarting the poll. */ +export function setOnnxDownloadProgressHandler( + handler: OnnxDownloadProgressHandler | undefined, +): void { + if (active) active.onProgress = handler; +} + +export function stopWatchingOnnxDownload(): void { + if (!active) return; + clearInterval(active.timer); + active = null; +} + +export function watchOnnxDownload(opts: WatchOptions): void { + const { + modelId, + onProgress, + onTerminal, + intervalMs = 500, + maxTicks = 600, + } = opts; + + stopWatchingOnnxDownload(); + + const watch: ActiveWatch = { + modelId, + onProgress, + onTerminal, + ticks: 0, + maxTicks, + finished: false, + timer: setInterval(() => { + void tick(); + }, intervalMs), + }; + active = watch; + + async function tick(): Promise { + if (!active || active !== watch || watch.finished) return; + watch.ticks += 1; + try { + const state = await onnxModelApi.getDownloadStatus(); + watch.onProgress?.(state); + if (state.status === "done" || state.status === "failed") { + await finish(state); + return; + } + if (watch.ticks >= watch.maxTicks) { + await finish({ + status: "failed", + progress: state.progress ?? 0, + error: "download timed out", + model_name: modelId, + }); + } + } catch (err) { + await finish({ + status: "failed", + progress: 0, + error: err instanceof Error ? err.message : String(err), + model_name: modelId, + }); + } + } + + async function finish(state: OnnxDownloadState): Promise { + if (watch.finished) return; + watch.finished = true; + clearInterval(watch.timer); + if (active === watch) active = null; + await watch.onTerminal(state); + } + + void tick(); +} diff --git a/dashboard/src/api/modules/onnxModel.ts b/dashboard/src/api/modules/onnxModel.ts new file mode 100644 index 00000000..e88a4e01 --- /dev/null +++ b/dashboard/src/api/modules/onnxModel.ts @@ -0,0 +1,90 @@ +import { request } from "../request"; + +export interface OnnxCatalogItem { + id: string; + name: string; + recommended: boolean; + downloaded: boolean; + size_gb?: number | null; + hf_source?: string | null; +} + +export interface OnnxModelMeta { + id: string; + size_gb?: number | null; + hf_source?: string | null; + supported?: boolean; + downloaded?: boolean; +} + +export interface OnnxDownloadState { + status: "idle" | "downloading" | "loading" | "done" | "failed"; + progress: number; + error?: string | null; + model_name: string; + task_id?: string; +} + +export interface OnnxServiceStatus { + enabled: boolean; + model: string; + ready: boolean; + downloaded: boolean; + cache_dir: string; + download: OnnxDownloadState; + local_models: string[]; + presets: string[]; + download_started?: boolean; + deps_available?: boolean; + deps_just_installed?: boolean; +} + +export const onnxModelApi = { + getCatalog: () => request("/onnx-models/catalog"), + getModelMeta: (model: string) => + request( + `/onnx-models/models/${encodeURIComponent(model)}/meta`, + ), + getStatus: () => request("/onnx-models/status"), + updateConfig: (body: { + enabled: boolean; + model: string; + download_if_missing?: boolean; + }) => + request("/onnx-models/config", { + method: "PUT", + body: JSON.stringify(body), + }), + download: (model: string) => + request("/onnx-models/download", { + method: "POST", + body: JSON.stringify({ model }), + }), + test: (model: string) => + request<{ + ok: boolean; + latency_ms?: number | null; + error?: string | null; + dim?: number | null; + }>("/onnx-models/test", { + method: "POST", + body: JSON.stringify({ model }), + }), + getDownloadStatus: () => + request("/onnx-models/download-status"), + deleteLocal: (model: string) => + request<{ ok: boolean; removed: boolean; status: OnnxServiceStatus }>( + `/onnx-models/local/${encodeURIComponent(model)}`, + { method: "DELETE" }, + ), + + /** Toggle local ONNX service without forcing a download. */ + setService: async (enabled: boolean) => { + const st = await onnxModelApi.getStatus(); + return onnxModelApi.updateConfig({ + enabled, + model: st.model || st.presets[0] || "BAAI/bge-small-zh-v1.5", + download_if_missing: false, + }); + }, +}; diff --git a/dashboard/src/api/modules/publishedExperts.test.ts b/dashboard/src/api/modules/publishedExperts.test.ts new file mode 100644 index 00000000..ecad37c6 --- /dev/null +++ b/dashboard/src/api/modules/publishedExperts.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { request } = vi.hoisted(() => ({ request: vi.fn() })); + +vi.mock("../request", () => ({ request })); + +import { publishedExpertsApi } from "./publishedExperts"; + +beforeEach(() => { + request.mockClear(); +}); + +describe("publishedExpertsApi", () => { + it("uses publish lifecycle endpoints for an agent-owned template", () => { + publishedExpertsApi.list(); + publishedExpertsApi.publish("agent-1", { + name: "Research assistant", + description: "Finds sources", + slug: "research-assistant", + }); + publishedExpertsApi.refresh("expert/1", { + name: "Updated", + description: "New description", + welcome_message: { zh: "欢迎", en: "Welcome" }, + }); + publishedExpertsApi.unpublish("expert/1"); + + expect(request).toHaveBeenNthCalledWith(1, "/experts/published"); + expect(request).toHaveBeenNthCalledWith( + 2, + "/agents/agent-1/publish-expert", + { + method: "POST", + body: JSON.stringify({ + name: "Research assistant", + description: "Finds sources", + slug: "research-assistant", + }), + }, + ); + expect(request).toHaveBeenNthCalledWith( + 3, + "/experts/published/expert%2F1/refresh", + { + method: "POST", + }, + ); + expect(request).toHaveBeenNthCalledWith( + 4, + "/experts/published/expert%2F1", + { + method: "DELETE", + }, + ); + }); + + it("installs a published template with its chosen agent details", () => { + publishedExpertsApi.install("expert-1", { + name: "My research assistant", + description: "Personal copy", + }); + + expect(request).toHaveBeenCalledWith( + "/experts/published/expert-1/install", + { + method: "POST", + body: JSON.stringify({ + name: "My research assistant", + description: "Personal copy", + }), + }, + ); + }); +}); diff --git a/dashboard/src/api/modules/publishedExperts.ts b/dashboard/src/api/modules/publishedExperts.ts new file mode 100644 index 00000000..eb61f1ce --- /dev/null +++ b/dashboard/src/api/modules/publishedExperts.ts @@ -0,0 +1,77 @@ +import { request } from "../request"; + +export interface PublishedExpert { + id: string; + slug: string; + name: string; + description: string; + created_by: string; + creator_username: string | null; + source_agent_id: string | null; + icon_name: string | null; + color: string | null; + created_at: string; + updated_at: string; +} + +export interface PublishExpertBody { + name: string; + description?: string; + slug?: string; + welcome_message?: { zh?: string; en?: string }; +} + +export type RefreshPublishedExpertBody = PublishExpertBody; + +export interface InstallPublishedExpertBody { + name: string; + description?: string; + providers?: string[]; + default_model?: string; + backend?: Record; + skill_package_ids?: string[]; + color?: string; + max_iters?: number | null; + max_input_length?: number | null; + temperature?: number | null; + top_p?: number | null; + max_tokens?: number | null; +} + +export interface InstalledPublishedExpert { + agent_id: string; + name: string; + description: string | null; + published_expert_id: string; +} + +const publishedPath = (expertId: string) => + `/experts/published/${encodeURIComponent(expertId)}`; + +export const publishedExpertsApi = { + list: () => request("/experts/published"), + + publish: (agentId: string, body: PublishExpertBody) => + request( + `/agents/${encodeURIComponent(agentId)}/publish-expert`, + { + method: "POST", + body: JSON.stringify(body), + }, + ), + + refresh: (expertId: string, body?: RefreshPublishedExpertBody) => + request(`${publishedPath(expertId)}/refresh`, { + method: "POST", + body: body ? JSON.stringify(body) : undefined, + }), + + unpublish: (expertId: string) => + request(publishedPath(expertId), { method: "DELETE" }), + + install: (expertId: string, body: InstallPublishedExpertBody) => + request(`${publishedPath(expertId)}/install`, { + method: "POST", + body: JSON.stringify(body), + }), +}; diff --git a/dashboard/src/api/modules/security.ts b/dashboard/src/api/modules/security.ts index 190669dd..f6b1dd19 100644 --- a/dashboard/src/api/modules/security.ts +++ b/dashboard/src/api/modules/security.ts @@ -60,6 +60,18 @@ export interface ToolGuardRulesSaveResponse { rule_count: number; } +export interface HitlToolCatalogItem { + name: string; + label_zh: string; + label_en: string; +} + +export interface SecurityDefaults { + hitl_tools: string[]; + hitl_tool_catalog: HitlToolCatalogItem[]; + tool_guard_rules: ToolGuardRule[]; +} + export interface SecurityPolicy { hitl: HitlPolicy; filesystem: FilesystemPolicy; @@ -103,4 +115,7 @@ export const securityApi = { }, ); }, + getDefaults(): Promise { + return request("/admin/security/defaults"); + }, }; diff --git a/dashboard/src/api/modules/sso.test.ts b/dashboard/src/api/modules/sso.test.ts new file mode 100644 index 00000000..3615509b --- /dev/null +++ b/dashboard/src/api/modules/sso.test.ts @@ -0,0 +1,37 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { request } = vi.hoisted(() => ({ request: vi.fn() })); + +vi.mock("../request", () => ({ request })); + +import { ssoApi } from "./sso"; + +describe("ssoApi", () => { + beforeEach(() => { + request.mockReset(); + }); + + it("uses the admin OIDC configuration endpoints", async () => { + const body = { + enabled: true, + display_name: "Acme SSO", + issuer: "https://identity.example.com", + client_id: "octop", + scopes: "openid profile email", + dashboard_origin: "https://octop.example.com", + }; + + await ssoApi.getOidcConfig(); + await ssoApi.putOidcConfig(body); + await ssoApi.testOidcConfig(); + + expect(request).toHaveBeenNthCalledWith(1, "/auth/oidc/config"); + expect(request).toHaveBeenNthCalledWith(2, "/auth/oidc/config", { + method: "PUT", + body: JSON.stringify(body), + }); + expect(request).toHaveBeenNthCalledWith(3, "/auth/oidc/config/test", { + method: "POST", + }); + }); +}); diff --git a/dashboard/src/api/modules/sso.ts b/dashboard/src/api/modules/sso.ts new file mode 100644 index 00000000..c6894d2e --- /dev/null +++ b/dashboard/src/api/modules/sso.ts @@ -0,0 +1,44 @@ +import { request } from "../request"; + +export interface OidcConfig { + enabled: boolean; + display_name: string; + issuer: string; + client_id: string; + scopes: string; + dashboard_origin: string | null; + has_client_secret: boolean; + redirect_uri?: string; +} + +export interface OidcConfigPut { + enabled: boolean; + display_name: string; + issuer: string; + client_id: string; + client_secret?: string; + scopes: string; + dashboard_origin?: string | null; +} + +export interface OidcConfigTestResult { + ok: boolean; + detail?: string; +} + +export const ssoApi = { + getOidcConfig(): Promise { + return request("/auth/oidc/config"); + }, + putOidcConfig(body: OidcConfigPut): Promise { + return request("/auth/oidc/config", { + method: "PUT", + body: JSON.stringify(body), + }); + }, + testOidcConfig(): Promise { + return request("/auth/oidc/config/test", { + method: "POST", + }); + }, +}; diff --git a/dashboard/src/api/modules/subagents.ts b/dashboard/src/api/modules/subagents.ts index d4a53035..60e20e23 100644 --- a/dashboard/src/api/modules/subagents.ts +++ b/dashboard/src/api/modules/subagents.ts @@ -31,6 +31,7 @@ export interface AgentSubagentSummary { description?: string; path: string; emoji?: string; + color?: string | null; } export function listSubagentDivisions(): Promise { diff --git a/dashboard/src/api/request.ts b/dashboard/src/api/request.ts index d57507ee..12697285 100644 --- a/dashboard/src/api/request.ts +++ b/dashboard/src/api/request.ts @@ -10,6 +10,8 @@ const AUTH_TOKEN_KEY = "auth_token"; * the SPA alive instead of tearing the document down mid-render. */ export const UNAUTHORIZED_EVENT = "octop:unauthorized"; +/** Fired when an agent-scoped API request is forbidden. */ +export const FORBIDDEN_EVENT = "octop:forbidden"; /** Response header used by the server for JWT sliding renewal. */ export const ACCESS_TOKEN_RESPONSE_HEADER = "X-Octop-Access-Token"; @@ -259,6 +261,11 @@ export async function request( applyRenewedAccessToken(response); if (!response.ok) { + if (response.status === 403 && isAgentScopedPath(path)) { + window.dispatchEvent( + new CustomEvent(FORBIDDEN_EVENT, { detail: { path } }), + ); + } const text = await response.text().catch(() => ""); throw new Error( `Request failed: ${response.status} ${response.statusText}${ diff --git a/dashboard/src/assets/providers/index.ts b/dashboard/src/assets/providers/index.ts index a6ce0763..806dd73b 100644 --- a/dashboard/src/assets/providers/index.ts +++ b/dashboard/src/assets/providers/index.ts @@ -9,6 +9,7 @@ import siliconLogo from "./silicon.png"; import groqLogo from "./groq.png"; import modelscopeLogo from "./modelscope.png"; import ollamaLogo from "./ollama.png"; +import onnxLogo from "./onnx.svg"; import tencentCodingPlanLogo from "./tencent-coding-plan.png"; import tencentTokenPlanLogo from "./tencent-token-plan.png"; import openrouterLogo from "./openrouter.png"; @@ -35,6 +36,7 @@ export const PROVIDER_LOGOS: Record = { groq: groqLogo, modelscope: modelscopeLogo, ollama: ollamaLogo, + onnx: onnxLogo, "tencent-coding-plan": tencentCodingPlanLogo, "tencent-token-plan": tencentTokenPlanLogo, "tencent-hai": tencentCodingPlanLogo, @@ -75,6 +77,7 @@ export function getProviderLogo(providerId: string): string | undefined { */ export const PROVIDER_DOCS: Record = { ollama: "https://ollama.com/search", + onnx: "https://onnx.ai/", openai: "https://platform.openai.com/docs", anthropic: "https://docs.anthropic.com", gemini: "https://ai.google.dev/gemini-api/docs", diff --git a/dashboard/src/assets/providers/onnx.svg b/dashboard/src/assets/providers/onnx.svg new file mode 100644 index 00000000..8bb19eb4 --- /dev/null +++ b/dashboard/src/assets/providers/onnx.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dashboard/src/components/AgentSelector.tsx b/dashboard/src/components/AgentSelector.tsx index 2d9afd95..1ed5d1e4 100644 --- a/dashboard/src/components/AgentSelector.tsx +++ b/dashboard/src/components/AgentSelector.tsx @@ -1,6 +1,8 @@ import { Select, Spin } from "antd"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useAgent, type OctopAgent } from "../context/AgentContext"; +import { ownedExperts } from "../utils/sharedExpert"; import { iconForName } from "../pages/Experts/components/iconForName"; import styles from "./AgentSelector.module.less"; @@ -56,6 +58,18 @@ export default function AgentSelector({ }: AgentSelectorProps) { const { t } = useTranslation(); const { agents, activeAgentId, setActiveAgent, loading } = useAgent(); + const selectable = useMemo(() => ownedExperts(agents), [agents]); + + useEffect(() => { + if (loading || selectable.length === 0) return; + if ( + activeAgentId && + selectable.some((agent) => agent.agent_id === activeAgentId) + ) { + return; + } + setActiveAgent(selectable[0]?.agent_id ?? null); + }, [activeAgentId, loading, selectable, setActiveAgent]); if (loading) { return ( @@ -65,11 +79,11 @@ export default function AgentSelector({ ); } - if (agents.length === 0) return null; + if (selectable.length === 0) return null; - const currentId = activeAgentId ?? agents[0]?.agent_id; + const currentId = activeAgentId ?? selectable[0]?.agent_id; const useBar = - variant === "bar" || (variant === "auto" && agents.length <= 6); + variant === "bar" || (variant === "auto" && selectable.length <= 6); return (

@@ -83,7 +97,7 @@ export default function AgentSelector({ role="tablist" aria-label={t("agentSelector.label")} > - {agents.map((agent) => ( + {selectable.map((agent) => ( { + options={selectable.map((agent) => { const accent = agentAccent(agent); return { value: agent.agent_id, @@ -116,7 +130,7 @@ export default function AgentSelector({ }; })} optionRender={(opt) => { - const agent = agents.find((a) => a.agent_id === opt.value); + const agent = selectable.find((a) => a.agent_id === opt.value); if (!agent) return opt.label; const accent = agentAccent(agent); return ( diff --git a/dashboard/src/components/AuthGuard.tsx b/dashboard/src/components/AuthGuard.tsx index b9470841..d0629cb1 100644 --- a/dashboard/src/components/AuthGuard.tsx +++ b/dashboard/src/components/AuthGuard.tsx @@ -2,8 +2,9 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Spin } from "antd"; import { getAuthToken } from "../api/request"; -import { authApi } from "../api/modules/auth"; +import { authApi, type OctopUser } from "../api/modules/auth"; import { applyUserLocale } from "../utils/locale"; +import { CurrentUserProvider } from "../hooks/useCurrentUser"; interface AuthGuardProps { children: React.ReactNode; @@ -24,6 +25,7 @@ export default function AuthGuard({ children }: AuthGuardProps) { const hadToken = Boolean(getAuthToken()); const [checking, setChecking] = useState(!hadToken); const [authed, setAuthed] = useState(hadToken); + const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; @@ -56,6 +58,7 @@ export default function AuthGuard({ children }: AuthGuardProps) { const me = await authApi.me(); await applyUserLocale(me.locale); if (!cancelled) { + setUser(me); setAuthed(true); setChecking(false); } @@ -98,5 +101,9 @@ export default function AuthGuard({ children }: AuthGuardProps) { ); } - return <>{children}; + return ( + + {children} + + ); } diff --git a/dashboard/src/components/CopyableResourceId.module.less b/dashboard/src/components/CopyableResourceId.module.less new file mode 100644 index 00000000..5eccc4b7 --- /dev/null +++ b/dashboard/src/components/CopyableResourceId.module.less @@ -0,0 +1,79 @@ +.root { + display: flex; + align-items: center; + gap: 4px; + margin: 0; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + max-width: 100%; + text-align: left; + color: var(--fn-text-tertiary); + transition: color 0.15s; + + &:hover { + color: var(--fn-text-secondary); + + .value { + color: var(--fn-text-primary); + } + } +} + +.label { + flex-shrink: 0; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.75; +} + +.value { + font-size: 11px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--fn-text-secondary); + transition: color 0.15s; +} + +.icon { + flex-shrink: 0; + opacity: 0.5; +} + +.inlineRoot { + display: inline-flex; + align-items: center; + gap: 4px; + margin: 0; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + max-width: 100%; + text-align: left; + color: var(--fn-text-tertiary); + font-size: 12px; + line-height: 1.5715; + transition: color 0.15s; + + &:hover { + color: var(--fn-text-secondary); + } +} + +.inlineText { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.inlineIcon { + flex-shrink: 0; + opacity: 0.55; +} diff --git a/dashboard/src/components/CopyableResourceId.tsx b/dashboard/src/components/CopyableResourceId.tsx new file mode 100644 index 00000000..f4bf89eb --- /dev/null +++ b/dashboard/src/components/CopyableResourceId.tsx @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import { Tooltip } from "antd"; +import { Copy } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { message } from "@/utils/antdMessage"; +import styles from "./CopyableResourceId.module.less"; + +type CopyableResourceIdProps = { + label: string; + value: string; + copyTitle?: string; + className?: string; + /** Match surrounding meta text (e.g. creator line). */ + inline?: boolean; +}; + +export function CopyableResourceId({ + label, + value, + copyTitle, + className, + inline = false, +}: CopyableResourceIdProps) { + const { t } = useTranslation(); + + const copy = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + message.success(t("common.copied")); + } catch { + message.error(t("common.copyFailed")); + } + }, [t, value]); + + return ( + + + + ); +} diff --git a/dashboard/src/components/EmojiPicker.module.less b/dashboard/src/components/EmojiPicker.module.less new file mode 100644 index 00000000..80fd0f6e --- /dev/null +++ b/dashboard/src/components/EmojiPicker.module.less @@ -0,0 +1,110 @@ +.overlay { + :global(.ant-popover-inner) { + padding: 0; + } +} + +.panel { + width: min(320px, 72vw); + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; +} + +.search { + flex-shrink: 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: 4px; + max-height: 240px; + overflow: auto; + padding: 2px; +} + +.option { + width: 100%; + aspect-ratio: 1; + min-height: 34px; + padding: 0; + border: 1px solid transparent; + border-radius: var(--fn-radius-md, 8px); + background: transparent; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + line-height: 1; + cursor: pointer; + transition: + background-color var(--fn-transition-fast, 0.15s ease), + border-color var(--fn-transition-fast, 0.15s ease); + + &:hover { + background: var(--fn-bg-hover); + } + + &:focus-visible { + outline: 2px solid var(--fn-border-focus); + outline-offset: 1px; + } +} + +.active { + border-color: var(--fn-text-primary); + background: var(--fn-color-brand-bg, rgba(79, 110, 247, 0.12)); +} + +.empty { + grid-column: 1 / -1; + padding: 16px 8px; + text-align: center; + font-size: 13px; + color: var(--fn-text-tertiary); +} + +.trigger { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 36px; + padding: 4px 12px 4px 8px; + border: 1px solid var(--fn-border-input); + border-radius: var(--fn-radius-md); + background: var(--fn-bg-primary); + color: var(--fn-text-primary); + cursor: pointer; + transition: + border-color var(--fn-transition-fast), + box-shadow var(--fn-transition-fast); + + &:hover { + border-color: var(--fn-border-focus); + } + + &:focus-visible { + outline: none; + border-color: var(--fn-border-focus); + box-shadow: 0 0 0 2px var(--fn-color-brand-shadow); + } +} + +.triggerEmoji { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: var(--fn-bg-secondary); + font-size: 18px; + line-height: 1; +} + +.triggerHint { + font-size: 13px; + color: var(--fn-text-secondary); +} diff --git a/dashboard/src/components/EmojiPicker.test.tsx b/dashboard/src/components/EmojiPicker.test.tsx new file mode 100644 index 00000000..2153ee68 --- /dev/null +++ b/dashboard/src/components/EmojiPicker.test.tsx @@ -0,0 +1,24 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { I18nextProvider } from "react-i18next"; +import i18n from "../i18n"; +import EmojiPicker from "./EmojiPicker"; + +describe("EmojiPicker", () => { + it("opens a selectable emoji grid and reports the chosen emoji", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + render( + + + , + ); + + await user.click(screen.getByRole("button", { name: /emoji/i })); + const option = await screen.findByRole("option", { name: "⚙️" }); + await user.click(option); + + expect(onChange).toHaveBeenCalledWith("⚙️"); + }); +}); diff --git a/dashboard/src/components/EmojiPicker.tsx b/dashboard/src/components/EmojiPicker.tsx new file mode 100644 index 00000000..ac33ca7f --- /dev/null +++ b/dashboard/src/components/EmojiPicker.tsx @@ -0,0 +1,103 @@ +import { useMemo, useState } from "react"; +import { Input, Popover } from "antd"; +import { useTranslation } from "react-i18next"; +import { SUBAGENT_EMOJI_OPTIONS } from "../utils/subagentEmojis"; +import styles from "./EmojiPicker.module.less"; + +interface EmojiPickerProps { + value?: string; + onChange?: (emoji: string) => void; + /** Shown when value is empty (subagents default 🤖, skills ✨). */ + fallback?: string; +} + +/** Popover grid of catalog emojis for subagent / skill icon selection. */ +export default function EmojiPicker({ + value, + onChange, + fallback = "🤖", +}: EmojiPickerProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const options = useMemo(() => { + const q = query.trim(); + if (!q) return SUBAGENT_EMOJI_OPTIONS as readonly string[]; + // Exact / substring match so pasted or partially typed emoji still filters. + return SUBAGENT_EMOJI_OPTIONS.filter((emoji) => emoji.includes(q)); + }, [query]); + + const current = (value || "").trim() || fallback; + + const panel = ( +
+ setQuery(e.target.value)} + placeholder={t("common.emojiSearchPlaceholder")} + aria-label={t("common.emojiSearchPlaceholder")} + className={styles.search} + /> +
+ {options.map((emoji) => { + const active = emoji === current; + return ( + + ); + })} + {options.length === 0 ? ( +
{t("common.emojiNoResults")}
+ ) : null} +
+
+ ); + + return ( + { + setOpen(next); + if (!next) setQuery(""); + }} + content={panel} + placement="bottomLeft" + arrow={false} + overlayClassName={styles.overlay} + > + + + ); +} diff --git a/dashboard/src/components/ExpertColorPicker.tsx b/dashboard/src/components/ExpertColorPicker.tsx new file mode 100644 index 00000000..fbdcfbbc --- /dev/null +++ b/dashboard/src/components/ExpertColorPicker.tsx @@ -0,0 +1,47 @@ +import { Tooltip } from "antd"; +import { useTranslation } from "react-i18next"; +import { + PALETTE_SWATCH, + VALID_PALETTES, + type ThemePalette, +} from "../styles/themePalettes"; +import styles from "./PaletteSwitcher.module.less"; + +interface ExpertColorPickerProps { + value: ThemePalette; + onChange: (palette: ThemePalette) => void; +} + +/** Curated 8-swatch picker for expert/agent accent color (list cards). */ +export default function ExpertColorPicker({ + value, + onChange, +}: ExpertColorPickerProps) { + const { t } = useTranslation(); + + return ( +
+ {VALID_PALETTES.map((key) => { + const active = value === key; + const label = t(`header.palette.${key}`); + return ( + + + + ); + })} +
+ ); +} diff --git a/dashboard/src/components/ForbiddenPage.tsx b/dashboard/src/components/ForbiddenPage.tsx new file mode 100644 index 00000000..e3aef256 --- /dev/null +++ b/dashboard/src/components/ForbiddenPage.tsx @@ -0,0 +1,33 @@ +import { Button, Result } from "antd"; +import { ShieldOff } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; + +/** Full-area "no permission" placeholder used by route and tab guards. */ +export default function ForbiddenPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + return ( +
+ } + title={t("common.noPermission")} + subTitle={t("common.noPermissionHint")} + extra={ + + } + /> +
+ ); +} diff --git a/dashboard/src/components/RequireAdmin.tsx b/dashboard/src/components/RequireAdmin.tsx deleted file mode 100644 index 6eeeefe8..00000000 --- a/dashboard/src/components/RequireAdmin.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Spin } from "antd"; -import { Navigate } from "react-router-dom"; -import { useUserRole } from "../hooks/useUserRole"; - -interface Props { - children: React.ReactNode; -} - -/** - * Wraps admin-only routes. Redirects to /chat for non-admins. - * Shows a full-screen spinner while the role is loading to avoid - * a flash of the admin page before the redirect fires. - */ -export default function RequireAdmin({ children }: Props) { - const role = useUserRole(); - - if (role === null) { - // Loading — show spinner. - return ( -
- -
- ); - } - - if (role !== "admin") { - return ; - } - - return <>{children}; -} diff --git a/dashboard/src/components/RequirePermission.tsx b/dashboard/src/components/RequirePermission.tsx new file mode 100644 index 00000000..74520156 --- /dev/null +++ b/dashboard/src/components/RequirePermission.tsx @@ -0,0 +1,40 @@ +import { Spin } from "antd"; +import { useLocation } from "react-router-dom"; +import { useCurrentUser } from "../hooks/useCurrentUser"; +import { canAccessPath } from "../utils/permissions"; +import ForbiddenPage from "./ForbiddenPage"; + +interface Props { + children: React.ReactNode; +} + +/** + * Wraps routes that have a module permission (or admin-only paths). + * Shows a spinner while /auth/me is loading, and a permission-denied + * page when the current user cannot access the path. + */ +export default function RequirePermission({ children }: Props) { + const user = useCurrentUser(); + const location = useLocation(); + + if (user === null) { + return ( +
+ +
+ ); + } + + if (!canAccessPath(user, location.pathname)) { + return ; + } + + return <>{children}; +} diff --git a/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.module.less b/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.module.less index f24d2ea8..41d2bec0 100644 --- a/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.module.less +++ b/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.module.less @@ -23,6 +23,10 @@ text-align: center; } +.wide .card { + width: min(100%, 480px); +} + .icon { display: flex; align-items: center; diff --git a/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.tsx b/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.tsx index f8f7cdfa..4b0207c4 100644 --- a/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.tsx +++ b/dashboard/src/components/StreamSetupGuide/StreamSetupGuide.tsx @@ -28,6 +28,9 @@ interface StreamSetupGuideProps { secondaryAction?: SetupGuideAction; /** Optional third action (e.g. uninstall) rendered after the main pair. */ extraAction?: SetupGuideAction; + className?: string; + /** Widen the card for longer explanatory copy. */ + wide?: boolean; } function ActionButton({ action }: { action: SetupGuideAction }) { @@ -55,11 +58,16 @@ export default function StreamSetupGuide({ primaryAction, secondaryAction, extraAction, + className, + wide, }: StreamSetupGuideProps) { const hasActions = primaryAction || secondaryAction || extraAction; + const wrapClass = [styles.wrap, wide ? styles.wide : "", className ?? ""] + .filter(Boolean) + .join(" "); return ( -
+
{icon}

{title}

diff --git a/dashboard/src/context/AgentContext.tsx b/dashboard/src/context/AgentContext.tsx index 71c47b89..c1439bb5 100644 --- a/dashboard/src/context/AgentContext.tsx +++ b/dashboard/src/context/AgentContext.tsx @@ -29,6 +29,10 @@ export interface OctopAgent { user_id?: number | null; /** Resolved username for admin list view. */ owner_username?: string | null; + /** Whether the owner has shared this expert with other users. */ + is_shared?: boolean; + /** Whether the current user owns this expert. */ + is_owner?: boolean; name: string; description: string | null; persona_mbti: string | null; diff --git a/dashboard/src/hooks/useCurrentUser.tsx b/dashboard/src/hooks/useCurrentUser.tsx new file mode 100644 index 00000000..288b681d --- /dev/null +++ b/dashboard/src/hooks/useCurrentUser.tsx @@ -0,0 +1,51 @@ +import { + createContext, + useContext, + useMemo, + type Dispatch, + type ReactNode, + type SetStateAction, +} from "react"; +import type { OctopUser } from "../api/modules/auth"; + +type CurrentUserContextValue = { + user: OctopUser | null; + setUser: Dispatch>; +}; + +const CurrentUserContext = createContext(null); + +export function CurrentUserProvider({ + user, + setUser, + children, +}: { + user: OctopUser | null; + setUser: Dispatch>; + children: ReactNode; +}) { + const value = useMemo(() => ({ user, setUser }), [user, setUser]); + return ( + + {children} + + ); +} + +/** + * Current authenticated user from AuthGuard's ``/auth/me`` result. + * ``null`` while loading or when the provider is not mounted. + */ +export function useCurrentUser(): OctopUser | null { + return useContext(CurrentUserContext)?.user ?? null; +} + +export function useSetCurrentUser(): Dispatch< + SetStateAction +> { + const ctx = useContext(CurrentUserContext); + if (!ctx) { + return () => undefined; + } + return ctx.setUser; +} diff --git a/dashboard/src/hooks/useGatedSearchTabs.ts b/dashboard/src/hooks/useGatedSearchTabs.ts new file mode 100644 index 00000000..3cd6aa13 --- /dev/null +++ b/dashboard/src/hooks/useGatedSearchTabs.ts @@ -0,0 +1,82 @@ +import { useEffect, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useCurrentUser } from "./useCurrentUser"; +import { userCanKey } from "../utils/permissions"; + +/** + * Search-param tabs filtered by module permission. + * Unknown / missing ``?tab=`` redirects to the first allowed tab. + * An explicit disallowed tab stays and ``forbidden`` is true. + */ +export function useGatedSearchTabs({ + tabs, + tabPermissions, + parseTab, + querylessKey, +}: { + tabs: readonly TTab[]; + tabPermissions: Record; + parseTab: (raw: string | null) => T; + querylessKey: T; +}) { + const user = useCurrentUser(); + const [searchParams, setSearchParams] = useSearchParams(); + const raw = searchParams.get("tab"); + const requested = parseTab(raw); + const explicitKnown = raw !== null && tabs.some((tab) => tab.key === raw); + + const allowedTabs = useMemo( + () => + user + ? tabs.filter((tab) => userCanKey(user, tabPermissions[tab.key])) + : [], + [tabs, user, tabPermissions], + ); + + const requestedAllowed = allowedTabs.some((tab) => tab.key === requested); + const forbidden = Boolean(user && explicitKnown && !requestedAllowed); + + useEffect(() => { + if (!user || forbidden || requestedAllowed) return; + const first = allowedTabs[0]; + if (!first) return; + const next = new URLSearchParams(searchParams); + if (first.key === querylessKey) { + if (!next.has("tab")) return; + next.delete("tab"); + } else { + next.set("tab", first.key); + } + setSearchParams(next, { replace: true }); + }, [ + user, + forbidden, + requestedAllowed, + allowedTabs, + querylessKey, + searchParams, + setSearchParams, + ]); + + const selectTab = (key: T) => { + const next = new URLSearchParams(searchParams); + if (key === querylessKey) { + next.delete("tab"); + } else { + next.set("tab", key); + } + setSearchParams(next, { replace: true }); + }; + + const activeTab: T = requestedAllowed + ? requested + : allowedTabs[0]?.key ?? requested; + + return { + user, + allowedTabs, + activeTab, + forbidden: forbidden || Boolean(user && allowedTabs.length === 0), + selectTab, + }; +} diff --git a/dashboard/src/hooks/useListPanelCollapsed.test.ts b/dashboard/src/hooks/useListPanelCollapsed.test.ts new file mode 100644 index 00000000..0bf4c255 --- /dev/null +++ b/dashboard/src/hooks/useListPanelCollapsed.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useListPanelCollapsed } from "./useListPanelCollapsed"; + +describe("useListPanelCollapsed", () => { + const key = "octop:test-list-collapsed"; + + beforeEach(() => { + localStorage.removeItem(key); + }); + + it("defaults to expanded when no storage entry", () => { + const { result } = renderHook(() => useListPanelCollapsed(key)); + expect(result.current.collapsed).toBe(false); + }); + + it("defaults to collapsed when defaultCollapsed is true", () => { + const { result } = renderHook(() => + useListPanelCollapsed(key, { defaultCollapsed: true }), + ); + expect(result.current.collapsed).toBe(true); + }); + + it("persists toggle state", () => { + const { result } = renderHook(() => useListPanelCollapsed(key)); + act(() => result.current.toggle()); + expect(result.current.collapsed).toBe(true); + expect(localStorage.getItem(key)).toBe("1"); + act(() => result.current.toggle()); + expect(result.current.collapsed).toBe(false); + expect(localStorage.getItem(key)).toBe("0"); + }); +}); diff --git a/dashboard/src/hooks/useListPanelCollapsed.ts b/dashboard/src/hooks/useListPanelCollapsed.ts new file mode 100644 index 00000000..b3571b3a --- /dev/null +++ b/dashboard/src/hooks/useListPanelCollapsed.ts @@ -0,0 +1,39 @@ +import { useCallback, useState } from "react"; + +function loadCollapsed(storageKey: string, defaultCollapsed: boolean): boolean { + try { + const stored = localStorage.getItem(storageKey); + if (stored === null) return defaultCollapsed; + return stored === "1"; + } catch { + return defaultCollapsed; + } +} + +function saveCollapsed(storageKey: string, collapsed: boolean) { + try { + localStorage.setItem(storageKey, collapsed ? "1" : "0"); + } catch { + /* ignore */ + } +} + +export function useListPanelCollapsed( + storageKey: string, + options?: { defaultCollapsed?: boolean }, +) { + const defaultCollapsed = options?.defaultCollapsed ?? false; + const [collapsed, setCollapsed] = useState(() => + loadCollapsed(storageKey, defaultCollapsed), + ); + + const toggle = useCallback(() => { + setCollapsed((prev) => { + const next = !prev; + saveCollapsed(storageKey, next); + return next; + }); + }, [storageKey]); + + return { collapsed, toggle, setCollapsed }; +} diff --git a/dashboard/src/hooks/usePathTabs.ts b/dashboard/src/hooks/usePathTabs.ts index 85d8102c..d71c3a1c 100644 --- a/dashboard/src/hooks/usePathTabs.ts +++ b/dashboard/src/hooks/usePathTabs.ts @@ -7,6 +7,8 @@ export interface UsePathTabsOptions { tabs: readonly T[]; storageKey: string; defaultTab: T; + /** When set, saved/bare fallback only uses allowed tabs. Disallowed URLs stay. */ + isAllowed?: (tab: T) => boolean; } export interface UsePathTabsResult { @@ -29,6 +31,7 @@ export function usePathTabs({ tabs, storageKey, defaultTab, + isAllowed, }: UsePathTabsOptions): UsePathTabsResult { const location = useLocation(); const navigate = useNavigate(); @@ -54,12 +57,12 @@ export function usePathTabs({ const readSaved = useCallback((): T => { try { const saved = localStorage.getItem(storageKey); - if (isTab(saved)) return saved; + if (isTab(saved) && (isAllowed?.(saved) ?? true)) return saved; } catch { /* ignore */ } return defaultTab; - }, [storageKey, isTab, defaultTab]); + }, [storageKey, isTab, defaultTab, isAllowed]); const pathTab = tabFromPath(location.pathname); const underBase = diff --git a/dashboard/src/hooks/useUpdateStatus.test.ts b/dashboard/src/hooks/useUpdateStatus.test.ts new file mode 100644 index 00000000..2963481f --- /dev/null +++ b/dashboard/src/hooks/useUpdateStatus.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { act, renderHook } from "@testing-library/react"; + +import type { UpdateStatus } from "../api/modules/update"; +import { + UPDATE_STATUS_CHANGED_EVENT, + UPDATE_STATUS_POLL_MS, + clearStoredUpdateStatus, + storeUpdateStatus, +} from "../utils/updateStatusCache"; + +const getUpdateStatus = vi.fn(); + +vi.mock("../api/modules/update", () => ({ + updateApi: { + getUpdateStatus: (...args: unknown[]) => getUpdateStatus(...args), + }, +})); + +import { useUpdateStatus } from "./useUpdateStatus"; + +const sample: UpdateStatus = { + current_version: "0.9.6", + latest_version: "0.9.7", + has_update: true, + is_editable: false, + service_mode: null, + error: null, + last_check_time: "2026-07-14T00:00:00Z", + release_notes: null, +}; + +describe("useUpdateStatus", () => { + beforeEach(() => { + localStorage.clear(); + getUpdateStatus.mockReset(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-14T12:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + clearStoredUpdateStatus(); + localStorage.clear(); + }); + + it("probes on mount when cache is empty", async () => { + getUpdateStatus.mockResolvedValue(sample); + const { result } = renderHook(() => useUpdateStatus()); + + await act(async () => { + await Promise.resolve(); + }); + + expect(getUpdateStatus).toHaveBeenCalledTimes(1); + expect(result.current.hasUpdate).toBe(true); + expect(result.current.status?.latest_version).toBe("0.9.7"); + }); + + it("re-probes after TTL via the poll interval", async () => { + getUpdateStatus.mockResolvedValue(sample); + renderHook(() => useUpdateStatus()); + + await act(async () => { + await Promise.resolve(); + }); + expect(getUpdateStatus).toHaveBeenCalledTimes(1); + + await act(async () => { + // One poll tick lands at TTL; cache is expired so the probe runs again. + vi.advanceTimersByTime(UPDATE_STATUS_POLL_MS); + await Promise.resolve(); + }); + + expect(getUpdateStatus.mock.calls.length).toBeGreaterThanOrEqual(2); + }); + + it("picks up status written by another screen", async () => { + getUpdateStatus.mockResolvedValue({ + ...sample, + has_update: false, + latest_version: "0.9.6", + }); + const { result } = renderHook(() => useUpdateStatus()); + + await act(async () => { + await Promise.resolve(); + }); + expect(result.current.hasUpdate).toBe(false); + + await act(async () => { + storeUpdateStatus(sample); + }); + + expect(result.current.hasUpdate).toBe(true); + expect(result.current.status?.latest_version).toBe("0.9.7"); + }); + + it("listens for the shared change event name", () => { + expect(UPDATE_STATUS_CHANGED_EVENT).toBe("octop:update-status-changed"); + }); +}); diff --git a/dashboard/src/hooks/useUpdateStatus.ts b/dashboard/src/hooks/useUpdateStatus.ts index bc596f59..76e8fb40 100644 --- a/dashboard/src/hooks/useUpdateStatus.ts +++ b/dashboard/src/hooks/useUpdateStatus.ts @@ -1,6 +1,8 @@ import { useCallback, useEffect, useState } from "react"; import { updateApi, type UpdateStatus } from "../api/modules/update"; import { + UPDATE_STATUS_CHANGED_EVENT, + UPDATE_STATUS_POLL_MS, isUpdateStatusCacheExpired, readStoredUpdateStatus, storeUpdateStatus, @@ -55,6 +57,25 @@ export function useUpdateStatus() { return () => window.removeEventListener("focus", onFocus); }, [refreshStatus]); + // Keep checking while the dashboard stays open (cache TTL still gates PyPI). + useEffect(() => { + const id = window.setInterval(() => { + void refreshStatus(false); + }, UPDATE_STATUS_POLL_MS); + return () => window.clearInterval(id); + }, [refreshStatus]); + + // Sync Header / Sidebar / Update page when any writer stores a new status. + useEffect(() => { + const onChanged = (event: Event) => { + const next = (event as CustomEvent).detail; + if (next) setStatus(next); + }; + window.addEventListener(UPDATE_STATUS_CHANGED_EVENT, onChanged); + return () => + window.removeEventListener(UPDATE_STATUS_CHANGED_EVENT, onChanged); + }, []); + const hasUpdate = Boolean(status?.has_update && status?.latest_version); return { status, hasUpdate, refreshStatus }; diff --git a/dashboard/src/hooks/useUserRole.ts b/dashboard/src/hooks/useUserRole.ts index 036a1da2..86afa05d 100644 --- a/dashboard/src/hooks/useUserRole.ts +++ b/dashboard/src/hooks/useUserRole.ts @@ -1,5 +1,4 @@ -import { useEffect, useState } from "react"; -import { authApi } from "../api/modules/auth"; +import { useCurrentUser } from "./useCurrentUser"; /** * Fetch the current user's role once on mount. @@ -7,20 +6,6 @@ import { authApi } from "../api/modules/auth"; * callers should treat null as "not admin" to avoid info leaks. */ export function useUserRole(): "admin" | "user" | null { - const [role, setRole] = useState<"admin" | "user" | null>(null); - useEffect(() => { - let cancelled = false; - authApi - .me() - .then((u) => { - if (!cancelled) setRole(u.role); - }) - .catch(() => { - // Non-200 (unauthenticated probe etc.) — leave role as null. - }); - return () => { - cancelled = true; - }; - }, []); - return role; + const user = useCurrentUser(); + return user?.role ?? null; } diff --git a/dashboard/src/layouts/MainLayout/index.tsx b/dashboard/src/layouts/MainLayout/index.tsx index 6b501d16..6172ddd2 100644 --- a/dashboard/src/layouts/MainLayout/index.tsx +++ b/dashboard/src/layouts/MainLayout/index.tsx @@ -14,13 +14,13 @@ import { MOBILE_FULLSCREEN_PATHS, SELF_HEADER_PATHS, isWorkbenchPath, - isControlAdminPath, } from "../../routes"; import { CHAT_HISTORY_RAIL_ID, isChatPath } from "../chatHistoryRail"; import { useIsMobile } from "../../hooks/useIsMobile"; import { useChatSidebarOpen } from "../../pages/Chat/hooks/useChatSidebarState"; import { EXPAND_CHAT_RAIL_EVENT } from "../../pages/Chat/components/ChatSidebarPanel"; -import RequireAdmin from "../../components/RequireAdmin"; +import RequirePermission from "../../components/RequirePermission"; +import { routeNeedsPermission } from "../../utils/permissions"; const Chat = lazy(() => import("../../pages/Chat")); const WorkbenchPage = lazy(() => import("../../pages/Control/Workbench")); @@ -159,8 +159,8 @@ export default function MainLayout() { {routeConfigs.map((rc) => { let el = rc.useWrapper ? : rc.element; - if (rc.path.startsWith("/admin/") || isControlAdminPath(rc.path)) { - el = {el}; + if (!rc.useWrapper && routeNeedsPermission(rc.path)) { + el = {el}; } return ; })} @@ -295,7 +295,7 @@ export default function MainLayout() { flexDirection: "column", }} > - + - +
)} diff --git a/dashboard/src/layouts/Sidebar.tsx b/dashboard/src/layouts/Sidebar.tsx index c8bd52ac..a756a894 100644 --- a/dashboard/src/layouts/Sidebar.tsx +++ b/dashboard/src/layouts/Sidebar.tsx @@ -16,6 +16,7 @@ import { Waypoints, Link2, Database, + Cpu, Users as UsersIcon, Activity, Share2, @@ -30,9 +31,10 @@ import { } from "lucide-react"; import { useTheme } from "../context/ThemeContext"; import { useUserRole } from "../hooks/useUserRole"; +import { useCurrentUser, useSetCurrentUser } from "../hooks/useCurrentUser"; import { useUpdateStatus } from "../hooks/useUpdateStatus"; -import { authApi } from "../api/modules/auth"; import type { OctopUser } from "../api/modules/auth"; +import { navAllowed } from "../utils/permissions"; import { prefetchRoute } from "../routes/prefetch"; import { useChatSidebarOpen } from "../pages/Chat/hooks/useChatSidebarState"; import { EXPAND_CHAT_RAIL_EVENT } from "../pages/Chat/components/ChatSidebarPanel"; @@ -124,7 +126,7 @@ interface NavSection { const iconSize = 16; const iconStroke = 1.8; -function buildNavSections(role: "admin" | "user" | null): NavSection[] { +function buildNavSections(user: OctopUser | null): NavSection[] { const sections: NavSection[] = [ { items: [ @@ -154,103 +156,135 @@ function buildNavSections(role: "admin" | "user" | null): NavSection[] { }, ], }, + ]; + + const settingsItems: NavItem[] = [ { - groupKey: "nav.settings", - items: [ - { - key: "personalization", - path: "/personalization/skills", - icon: , - labelKey: "nav.personalization", - }, - { - key: "channels", - path: "/personalization/channels", - icon: , - labelKey: "nav.channels", - }, - { - key: "connectors", - path: "/connectors", - icon: , - labelKey: "nav.connectors", - }, - { - key: "skill-packages", - path: "/skill-packages", - icon: , - labelKey: "nav.skillPackages", - }, - ], + key: "personalization", + path: "/personalization/skills", + icon: , + labelKey: "nav.personalization", }, ]; + if (navAllowed(user, "channels")) { + settingsItems.push({ + key: "channels", + path: "/personalization/channels", + icon: , + labelKey: "nav.channels", + }); + } + if (navAllowed(user, "connectors")) { + settingsItems.push({ + key: "connectors", + path: "/connectors", + icon: , + labelKey: "nav.connectors", + }); + } + if (navAllowed(user, "skill-packages")) { + settingsItems.push({ + key: "skill-packages", + path: "/skill-packages", + icon: , + labelKey: "nav.skillPackages", + }); + } + if (navAllowed(user, "knowledge-bases")) { + settingsItems.push({ + key: "knowledge-bases", + path: "/knowledge-bases", + icon: , + labelKey: "nav.knowledgeBases", + badge: "BETA", + }); + } + if (settingsItems.length > 0) { + sections.push({ groupKey: "nav.settings", items: settingsItems }); + } - if (role === "admin") { - sections.push({ - groupKey: "nav.control", - items: [ - { - key: "workbench", - path: "/workbench", - icon: , - labelKey: "nav.workbench", - }, - { - key: "remote-desktop", - path: "/remote-desktop", - icon: , - labelKey: "nav.remoteDesktop", - }, - { - key: "acp", - path: "/acp", - icon: , - labelKey: "nav.acp", - }, - ], + const controlItems: NavItem[] = []; + if (navAllowed(user, "workbench")) { + controlItems.push({ + key: "workbench", + path: "/workbench", + icon: , + labelKey: "nav.workbench", }); - sections.push({ - groupKey: "nav.admin", - items: [ - { - key: "admin-users", - path: "/admin/users", - icon: , - labelKey: "nav.adminUsers", - }, - { - key: "models", - path: "/admin/models", - icon: , - labelKey: "nav.models", - }, - { - key: "admin-storage", - path: "/admin/backend", - icon: , - labelKey: "nav.adminStorage", - }, - { - key: "admin-plugins", - path: "/admin/plugins", - icon: , - labelKey: "nav.adminPlugins", - }, - { - key: "admin-security", - path: "/admin/security", - icon: , - labelKey: "nav.security", - }, - { - key: "admin-advanced", - path: "/admin/advanced", - icon: , - labelKey: "nav.adminAdvanced", - }, - ], + } + if (navAllowed(user, "remote-desktop")) { + controlItems.push({ + key: "remote-desktop", + path: "/remote-desktop", + icon: , + labelKey: "nav.remoteDesktop", + }); + } + // ACP: no module key this round — admin role only. + if (navAllowed(user, "acp")) { + controlItems.push({ + key: "acp", + path: "/acp", + icon: , + labelKey: "nav.acp", + }); + } + if (controlItems.length > 0) { + sections.push({ groupKey: "nav.control", items: controlItems }); + } + + const adminItems: NavItem[] = []; + if (navAllowed(user, "admin-users")) { + adminItems.push({ + key: "admin-users", + path: "/admin/users", + icon: , + labelKey: "nav.adminUsers", + }); + } + if (navAllowed(user, "models")) { + adminItems.push({ + key: "models", + path: "/admin/models", + icon: , + labelKey: "nav.models", + }); + } + if (navAllowed(user, "admin-storage")) { + adminItems.push({ + key: "admin-storage", + path: "/admin/backend", + icon: , + labelKey: "nav.adminStorage", + }); + } + if (navAllowed(user, "admin-plugins")) { + adminItems.push({ + key: "admin-plugins", + path: "/admin/plugins", + icon: , + labelKey: "nav.adminPlugins", }); } + if (navAllowed(user, "admin-security")) { + adminItems.push({ + key: "admin-security", + path: "/admin/security", + icon: , + labelKey: "nav.security", + }); + } + if (navAllowed(user, "admin-advanced")) { + adminItems.push({ + key: "admin-advanced", + path: "/admin/advanced", + icon: , + labelKey: "nav.adminAdvanced", + }); + } + if (adminItems.length > 0) { + sections.push({ groupKey: "nav.admin", items: adminItems }); + } return sections; } @@ -405,8 +439,9 @@ function NavList({ }) { const { t } = useTranslation(); const role = useUserRole(); + const user = useCurrentUser(); const { hasUpdate } = useUpdateStatus(); - const navSections = buildNavSections(role); + const navSections = buildNavSections(user); const MOBILE_HIDDEN_KEYS = new Set(); @@ -503,23 +538,17 @@ export default function Sidebar({ const { t } = useTranslation(); const { isDark } = useTheme(); const role = useUserRole(); + const user = useCurrentUser(); + const setUser = useSetCurrentUser(); const { hasUpdate } = useUpdateStatus(); - const navSections = buildNavSections(role); + const navSections = buildNavSections(user); const { toggleGroup, isGroupCollapsed } = useNavGroupCollapse( navSections, selectedKey, ); - const [user, setUser] = useState(null); const [chatSidebarOpen, setChatSidebarOpen] = useChatSidebarOpen(); const showChatRailExpand = !chatSidebarOpen; - useEffect(() => { - authApi - .me() - .then(setUser) - .catch(() => {}); - }, []); - const isRailCollapsed = collapsed && !isMobile; const wordmarkSrc = isDark ? "/logo_name_dark.png" : "/logo_name.png"; diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index d7b0f2d6..be84f34d 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -18,6 +18,7 @@ "view": "View", "create": "Create", "add": "Add", + "actions": "Actions", "upload": "Upload", "download": "Download", "refresh": "Refresh", @@ -37,8 +38,15 @@ "askOctopHint": "If the install keeps failing, copy the log and ask Octop to help you troubleshoot.", "close": "Close", "contentPlaceholder": "Enter content...", + "emojiPick": "Choose emoji", + "emojiPickerLabel": "Emoji", + "emojiSearchPlaceholder": "Filter emoji", + "emojiNoResults": "No matching emoji", "gotIt": "Got it", "unknownError": "Unknown error", + "noPermission": "Permission denied", + "noPermissionHint": "You do not have access to this page. Ask an administrator to grant it.", + "backToChat": "Back to chat", "viewDetail": "View Details", "viewMore": "View More", "deleteFailed": "Delete failed", @@ -61,6 +69,7 @@ "SETUP_REQUIRED": "Initial setup is required.", "DATABASE_NOT_EMPTY": "The target database already has users. Use an empty database, or log in with the existing admin.", "BACKUP_DRIVER_MISMATCH": "This backup was made with a different database engine (SQLite vs PostgreSQL). Switch the runtime to match the backup, or create a new backup on the current engine. Cross-engine restore is not supported.", + "BACKUP_IN_PROGRESS": "A backup is already in progress. Try again shortly.", "FORBIDDEN": "Permission denied.", "NOT_FOUND": "Not found.", "USER_DISABLED": "This account has been disabled.", @@ -79,6 +88,7 @@ "PROVIDER_NAME_TAKEN": "Provider name is already in use.", "PROVIDER_NOT_VISIBLE": "Provider is not visible.", "PROVIDER_REFERENCED": "Provider is in use and cannot be removed.", + "PROVIDER_LOCAL_PROTECTED": "Local runtime providers cannot be deleted.", "STORAGE_BACKEND_NAME_TAKEN": "Storage backend name is already in use.", "STORAGE_BACKEND_REFERENCED": "Storage backend is in use and cannot be removed.", "CHANNEL_KIND_UNSUPPORTED": "Unsupported channel type.", @@ -109,13 +119,25 @@ "SKILL_PACKAGE_NOT_FOUND": "Skill package not found.", "SKILL_PACKAGE_NAME_TAKEN": "Skill package {{name}} already exists.", "SKILL_PACKAGE_BACKEND_UNSUPPORTED": "Skill packages currently support only “Full local environment (filesystem + shell)” or “Local filesystem (no shell commands)”, with storage root / (POSIX) or the agent workspace root (Windows default).", + "PUBLISHED_EXPERT_SLUG_TAKEN": "Published expert slug {{slug}} already exists.", + "PUBLISHED_EXPERT_ALREADY_EXISTS": "This agent is already published as “{{name}}”. Refresh the published template instead.", + "OIDC_BAD_REQUEST": "Invalid OIDC request: {{detail}}", "EXPERT_MARKET_FAILED": "Expert market is temporarily unavailable. Please try again later.", "SKILLHUB_SSL_FAILED": "SkillHub request failed: Python SSL error detected — on macOS try `brew reinstall openssl@3`, or check your system CA certificates.", "DESKTOP_SESSION_LIMIT": "Too many concurrent remote desktop streams (max {{limit}}). Close other connections and try again.", "DESKTOP_CAPTURE_FAILED": "Screen capture failed repeatedly. On Linux servers restart the virtual desktop; on macOS grant Screen Recording and Accessibility, then reconnect.", "PLUGIN_INVALID_ARCHIVE": "The download is not a valid plugin ZIP. Use a direct .zip download URL (on GitHub use raw.githubusercontent.com or the “Download” / “raw” link, not the /blob/ page).", "PLUGIN_INSTALL_FAILED": "Failed to download or install the plugin: {{reason}}", - "PLUGIN_ALREADY_EXISTS": "Plugin {{id}} is already installed." + "PLUGIN_ALREADY_EXISTS": "Plugin {{id}} is already installed.", + "KNOWLEDGE_FEATURE_DISABLED": "Knowledge bases are disabled by the administrator.", + "KNOWLEDGE_PREREQUISITES_FAILED": "Knowledge-base embedding prerequisites are not ready.", + "KNOWLEDGE_NOT_FOUND": "Knowledge base or document not found.", + "KNOWLEDGE_FORBIDDEN": "You do not have access to this knowledge base.", + "KNOWLEDGE_DOC_LIMIT": "A knowledge base can contain at most 100 documents.", + "KNOWLEDGE_DOC_TOO_LARGE": "This document exceeds the 20 MB size limit.", + "KNOWLEDGE_BASE_LIMIT": "You can create at most 20 knowledge bases.", + "KNOWLEDGE_UNSUPPORTED_TYPE": "This document type is not supported for knowledge bases.", + "KNOWLEDGE_NAME_TAKEN": "You already have a knowledge base with this name." }, "nav": { "chat": "Chat", @@ -162,13 +184,15 @@ "newVersionBadge": "New version", "admin": "Admin", "adminUsers": "Users", + "adminSso": "Single sign-on", "adminStorage": "Storage", "adminAudit": "Audit log", "adminAgents": "Agents", "adminAdvanced": "Application Settings", "security": "Security", "adminPlugins": "Plugins", - "skillPackages": "Skill Packages" + "skillPackages": "Skill Packages", + "knowledgeBases": "Knowledge Bases" }, "skillPackages": { "title": "Skill Packages", @@ -189,6 +213,14 @@ "fromSkillHubHint": "Import a SkillHub skillset as a new skill package.", "addAsPackage": "Add as package", "empty": "No skill packages yet", + "emptyGuideTitle": "No skill packages yet", + "emptyGuideDesc": "A skill package groups reusable skills so you can mount them on experts and use them in chat.", + "emptyGuideStepWhat": "What is a skill?", + "emptyGuideStepWhatDetail": "A skill is a playbook (SKILL.md) that tells an expert how to complete a task, such as research, filling forms, or calling tools.", + "emptyGuideStepHow": "How to use it", + "emptyGuideStepHowDetail": "Create a package, add skills or import them from the market, then mount the package on an expert. Those skills will be available in chat.", + "emptyGuideStepShare": "What sharing does", + "emptyGuideStepShareDetail": "Packages are visible to signed-in users on this instance. Others can mount them; only the creator can edit or delete.", "selectPackage": "Select a skill package to view its skills", "backToList": "Back to list", "noDescription": "No description", @@ -213,7 +245,157 @@ "skillSaved": "Skill saved", "skillDeleted": "Skill deleted", "resizeSidebar": "Resize package list", - "createdBy": "Created by {{name}}" + "createdBy": "Created by {{name}}", + "packageId": "Package ID", + "copyPackageId": "Click to copy package ID", + "collapseListPanel": "Collapse list", + "expandListPanel": "Expand list" + }, + "knowledgeBases": { + "title": "Knowledge Bases", + "subtitle": "Index documents for chat retrieval. Optionally share them with users on this instance.", + "featureTitle": "Enable Knowledge Bases", + "settingsTitle": "Knowledge base settings", + "settingsLead": "Instance-wide switch. When on, configure an embedding model. When off, knowledge bases are unavailable for everyone.", + "settingsOpen": "Enable knowledge bases", + "manageModels": "Manage models", + "unavailableTitle": "Knowledge bases are not ready", + "unavailableDescription": "Open Settings in the top-right to enable knowledge bases and configure an embedding model.", + "unavailableDescriptionNonAdmin": "Knowledge bases are not enabled or not configured yet. Please contact an administrator.", + "create": "Create knowledge base", + "edit": "Edit knowledge base", + "name": "Name", + "nameRequired": "Please enter a knowledge base name", + "description": "Description", + "empty": "No knowledge bases yet", + "emptyGuideTitle": "No knowledge bases yet", + "emptyGuideDesc": "Turn documents into a searchable library for chat citations, and optionally share them with users on this instance.", + "emptyGuideStepWhat": "What is a knowledge base?", + "emptyGuideStepWhatDetail": "A knowledge base splits and embeds uploaded documents so experts can retrieve relevant passages in chat, instead of stuffing the full text into context.", + "emptyGuideStepHow": "How to use it", + "emptyGuideStepHowDetail": "Create a knowledge base and upload md / txt / pdf / docx / pptx files. Turn on Default enabled, or select it manually in chat.", + "emptyGuideStepShare": "What sharing does", + "emptyGuideStepShareDetail": "When shared, everyone signed in on this instance can read and select it; only the owner can edit. Default enabled applies only to the owner.", + "enableGuideTitle": "How do I enable knowledge bases?", + "enableGuideDesc": "Knowledge bases are instance-wide. Turn the feature on, configure an embedding model, then create a knowledge base for chat retrieval.", + "enableGuideStepOpen": "Open knowledge base settings", + "enableGuideStepOpenDetail": "Click Knowledge base settings below to manage the instance switch and embedding model.", + "enableGuideStepToggle": "Enable knowledge bases", + "enableGuideStepToggleDetail": "Turn on Enable knowledge bases. When off, knowledge bases are unavailable for everyone.", + "enableGuideStepModel": "Configure an embedding model", + "enableGuideStepModelDetail": "Choose local ONNX (download the model first) or an online embedding provider, then save.", + "noDescription": "No description", + "selectBase": "Select a knowledge base to view its documents", + "backToList": "Back to list", + "createdBy": "Created by {{name}}", + "baseId": "Knowledge base ID", + "copyBaseId": "Click to copy knowledge base ID", + "collapseListPanel": "Collapse list", + "expandListPanel": "Expand list", + "documentCount": "{{count}} documents", + "documentTotal": "{{count}} documents in total", + "defaultOpen": "Default enabled", + "defaultOpenBadge": "Default on", + "defaultOpenHint": "Can be on together with Shared. When on, only your chats include this knowledge base by default. Other users who can see a shared base still select it manually. When off, you must select it in chat.", + "defaultOpenWarning": "When enabled, your chats include this knowledge base by default (using more context); it can be removed for this turn.", + "shared": "Shared", + "sharedBadge": "Shared", + "sharedHint": "Can be on together with Default enabled. Everyone signed in to this instance can read and select it; only the owner can edit it. Default-open applies only to the owner.", + "members": "Members", + "share": "Share", + "noMembers": "Only you can access this knowledge base.", + "member": "User {{id}} · {{role}}", + "userId": "User ID", + "userIdRequired": "Please enter a user ID", + "role": "Role", + "roles": { + "viewer": "Viewer", + "editor": "Editor" + }, + "documents": "Documents", + "documentLimit": "{{count}} / {{max}} documents", + "documentLimitReached": "This knowledge base already contains the maximum of {{count}} documents.", + "documentTooLarge": "Each document must be at most {{sizeMb}} MB.", + "baseLimitReached": "You can create at most {{count}} knowledge bases.", + "uploadHint": "Supports md / txt / pdf / docx / pptx. Max {{sizeMb}} MB per file.", + "viewCard": "Cards", + "viewTable": "Table", + "updatedAt": "Updated", + "rebuildDocument": "Rebuild index", + "rebuildDocumentConfirm": "Re-parse and re-embed this document?", + "rebuildDocumentSuccess": "Document reindex started", + "rebuildDocumentFailed": "Failed to rebuild document index", + "previewDocument": "Preview", + "previewEmpty": "(No text content)", + "previewFailed": "Failed to preview document", + "icon": "Icon", + "iconPlaceholder": "Choose a knowledge-base icon", + "iconLabels": { + "book-open": "Humanities", + "landmark": "History", + "users": "Social sciences", + "scale": "Law", + "flask-conical": "Science", + "atom": "STEM", + "wrench": "Technology", + "cpu": "Computing", + "terminal": "Engineering", + "graduation-cap": "Education", + "briefcase": "Business", + "palette": "Arts", + "languages": "Languages" + }, + "upload": "Upload documents", + "uploaded": "Documents uploaded", + "uploadFailed": "Failed to upload documents", + "emptyDocuments": "No documents yet", + "filename": "File name", + "status": "Status", + "chunks": "Chunks", + "chunkCount": "{{count}} chunks", + "statuses": { + "pending": "Pending", + "processing": "Processing", + "ready": "Ready", + "failed": "Failed" + }, + "statusesShort": { + "pending": "Pending", + "processing": "Indexing", + "ready": "Ready", + "failed": "Failed" + }, + "deleteConfirm": "Delete this knowledge base and all of its documents?", + "deleteDocumentConfirm": "Delete this document?", + "loadFailed": "Failed to load knowledge bases", + "saveFailed": "Failed to save knowledge base changes", + "deleteFailed": "Failed to delete", + "created": "Knowledge base created", + "updated": "Knowledge base updated", + "deleted": "Knowledge base deleted", + "resizeSidebar": "Resize knowledge base list", + "enableTitle": "Enable Knowledge Bases", + "enableDescription": "Choose a local ONNX or online embedding model. Changing the model rebuilds all knowledge indexes.", + "selectModel": "Select embedding model", + "selectProvider": "Select an online provider", + "noProviders": "No configured providers yet. Add one in model settings first.", + "noModels": "No embedding models available", + "localOnnx": "Local ONNX", + "remoteEmbedding": "Online embedding", + "rebuildConfirmTitle": "Rebuild all knowledge indexes?", + "rebuildConfirmDescription": "Changing the embedding model clears and re-embeds every knowledge index.", + "notDownloaded": "not downloaded", + "approxSize": "about {{size}}", + "sizeUnknown": "size unknown", + "recommended": "Recommended", + "showMoreOnnx": "Show more", + "downloadModel": "Download", + "downloadNeedModel": "The selected model is not downloaded yet. Download it before saving.", + "onnxServiceEnabled": "Local ONNX service enabled", + "enable": "Enable", + "featureEnabled": "Knowledge bases enabled", + "featureDisabled": "Knowledge bases disabled", + "featureSaveFailed": "Failed to update knowledge base feature" }, "setupWizard": { "title": "Setup Wizard", @@ -375,6 +557,45 @@ "subtitle": "Spin up a pre-configured agent (with identity / soul / heartbeat baked in) from a scenario template. Each expert ships with its own system prompt — once created, just bind a provider and start it.", "loadFailed": "Failed to load experts", "loadDetailFailed": "Failed to load expert details", + "share": { + "toggle": "Share with other users", + "badge": "Shared", + "fromOwner": "Shared expert · from {{name}}" + }, + "published": { + "title": "Publish template", + "badge": "Published", + "cardPublish": "Publish template", + "publishConfirmTitle": "Publish as template?", + "publishConfirm": "This creates a snapshot template. Other users can install a private copy under Built-in Experts → Published by users. Your chat history is not shared.", + "statusPublished": "This agent is published as a community template.", + "statusNotPublished": "Publish this agent so other users can install a private copy.", + "publish": "Publish", + "publishSuccess": "Template published", + "publishSuccessHint": "Template published. Others can install it under Built-in Experts → Published by users.", + "publishFailed": "Failed to publish template", + "update": "Update published template", + "updateSuccess": "Published template updated", + "updateFailed": "Failed to update published template", + "unpublish": "Unpublish", + "unpublishConfirm": "Unpublish this template? Existing private installs are not affected.", + "unpublishSuccess": "Template unpublished", + "unpublishFailed": "Failed to unpublish template", + "listTitle": "Published by users ({{count}})", + "listHint": "Templates published by users on this instance. Click Install to create your own private expert.", + "emptyHint": "No user-published templates yet. On My Experts, click the upload icon on a card to publish one.", + "by": "by {{name}}", + "install": "Install", + "installTitle": "Install \"{{name}}\"", + "installFailed": "Failed to install template", + "drawerTitle": "Publish expert template", + "drawerHint": "Only expert config markdown (SOUL, IDENTITY, …), skills, and sub-agent definitions are included. Runtime data such as memory DB, uploads, and built-in skills is excluded.", + "fieldName": "Template name", + "fieldDescription": "Description", + "fieldWelcomeZh": "Welcome message (Chinese)", + "fieldWelcomeEn": "Welcome message (English)", + "fieldWelcomePlaceholder": "One-line greeting shown on the chat welcome screen" + }, "noExperts": "No experts available", "selectExpertHint": "Pick an expert on the left to view details", "creating": "Creating…", @@ -473,6 +694,8 @@ "basicInfo": "Basic Info", "agentName": "Name", "agentDescription": "Description", + "color": "Color", + "colorHint": "Choose the accent color for list cards and chat avatars.", "defaultModel": "Default Model", "defaultModelAuto": "Auto (use provider default)", "editFile": "Edit →", @@ -597,6 +820,7 @@ "skillFilesHint": "Skills bundled with this expert. They will be written to the workspace skills/ directory after creation.", "noSkillFiles": "This expert has no bundled skills", "subagentFilesTitle": "Subagents ({{count}})", + "subagentTemplateHint": "Subagents bundled with this expert. They will be written to the workspace agents/ directory after creation.", "subagentFilesHint": "Subagents defined in the workspace agents/ directory. The main agent delegates tasks to them via the task tool.", "noSubagentFiles": "No subagents in workspace", "manageSubagents": "Manage subagents", @@ -645,8 +869,14 @@ "pleaseInputDescription": "Please enter a description", "emojiLabel": "Emoji", "emojiPlaceholder": "🤖", + "emojiHint": "Pick an icon for this subagent from the list.", + "emojiPick": "Choose emoji", + "emojiPickerLabel": "Emoji", + "emojiSearchPlaceholder": "Filter emoji", + "emojiNoResults": "No matching emoji", "colorLabel": "Color", "colorPlaceholder": "Optional, e.g. cyan or #6366f1", + "colorHint": "Choose a color for the subagent card in the installed list.", "bodyLabel": "Instructions", "bodyPlaceholder": "Write the subagent personality and instructions in Markdown…", "pleaseInputBody": "Please enter instructions", @@ -690,6 +920,9 @@ "model_call_failed": "The model call failed after several retries. Wait a moment and retry. If it keeps failing, check the model settings or switch models." }, "chat": { + "sharedExpert": { + "banner": "Shared expert · provided by {{name}}" + }, "thinking": "Thinking", "continuing": "Continuing", "generating": "Generating", @@ -700,6 +933,11 @@ "refreshingMessages": "Refreshing messages…", "streamResumed": "Connection restored. Pull down to refresh if anything looks incomplete.", "regenerate": "Regenerate", + "forkFromHere": "Fork from here", + "forkSuccess": "Forked into a new chat", + "forkSuccessEmpty": "Forked into a new empty chat — edit your question and send", + "forkFailed": "Failed to fork this conversation", + "forkDisabledWhileBusy": "Wait for the current reply or approval to finish", "retry": "Retry", "like": "Helpful", "dislike": "Not helpful", @@ -765,6 +1003,10 @@ "title": "Expert details" }, "composerMore": "More tools", + "knowledgePicker": "Knowledge bases", + "knowledgePickerSearch": "Search knowledge bases", + "knowledgePickerEmpty": "No knowledge bases available", + "manageKnowledgeBases": "Manage knowledge bases", "skillPicker": "Select skills", "skillPickerSearch": "Search skills", "skillPickerEmpty": "No skills available", @@ -824,6 +1066,7 @@ "write_file": "Write file", "edit_file": "Edit file", "execute": "Execute", + "bash": "Shell (bash)", "current_time": "Current time", "write_todos": "Write plan", "task": "Sub-agent task", @@ -846,6 +1089,7 @@ "cronjob_update": "Update cron job", "cronjob_delete": "Delete cron job", "cronjob_run_now": "Run cron job now", + "search_knowledge": "Search knowledge base", "agent_list": "List agents", "ask_agent": "Ask agent", "call_agent": "Call agent", @@ -976,7 +1220,27 @@ "importConfirmOk": "Restore", "importSuccess": "Restore complete ({{agents}} agents, {{files}} workspace files)", "importFailed": "Failed to restore backup", - "restoreConfig": "Also restore config.json and env (requires a manual service restart to apply)" + "restoreConfig": "Also restore config.json and env (requires a manual service restart to apply)", + "autoTitle": "Automatic backup", + "autoDesc": "Schedule full system backups into the backups directory. Only automatic archives are pruned by retention.", + "autoEnabled": "Enable automatic backup", + "autoSchedule": "Schedule", + "autoScheduleDaily": "Daily at 04:00", + "autoScheduleWeekly": "Weekly on Sunday at 04:00", + "autoSchedule12h": "Every 12 hours", + "autoScheduleCustom": "Custom", + "autoScheduleHint": "cron:m h dom mon dow (server timezone); interval:, e.g. interval:43200 means every 12 hours.", + "autoIntervalPreview": "Current: every {{seconds}} seconds (about {{hours}} h)", + "autoRetention": "Keep last N automatic backups", + "autoSave": "Save settings", + "autoSaveSuccess": "Automatic backup settings saved", + "autoSaveFailed": "Failed to save automatic backup settings", + "autoLoadFailed": "Failed to load automatic backup settings", + "autoRunNow": "Run now", + "autoRunSuccess": "Automatic backup created", + "autoRunFailed": "Failed to run automatic backup", + "autoScheduled": "Scheduler: active", + "autoNotScheduled": "Scheduler: inactive" }, "skills": { "title": "Skills", @@ -994,6 +1258,8 @@ "invalidSkillUrlSource": "Enter a valid HTTP(S) skill URL; supported sources are validated by the server adapter", "zipHintTitle": "ZIP layout (each top-level folder = one skill):", "zipHintDetail": "Each folder must contain SKILL.md and may include scripts or other files. Only .zip is supported.", + "zipDragDropHint": "Drag and drop your files here to upload", + "zipSelected": "Selected: {{name}}", "chooseZip": "Choose ZIP file", "noZipSelected": "No file selected", "removeZip": "Remove", @@ -1014,6 +1280,12 @@ "createSkill": "Create Skill", "viewSkill": "View Skill", "editSkill": "Edit Skill", + "fileTreeTitle": "Skill files", + "fileTreeEmpty": "No files", + "fileTreeShow": "Show skill files", + "fileTreeHide": "Hide skill files", + "fileTreeAgentNotReady": "Agent is not running — skill files cannot be loaded", + "finishEditBeforeSwitchFile": "Save or cancel editing before switching files", "saveSkill": "Save", "viewPreview": "Preview", "viewSource": "Source", @@ -1102,6 +1374,8 @@ "addMetadata": "Add field", "metadataKeyRequired": "Key is required", "metadataValueRequired": "Value is required", + "emojiLabel": "Emoji", + "emojiHint": "Pick an icon for this skill from the list.", "bodyLabel": "Skill implementation", "bodyPlaceholder": "Describe when to use this skill and how to execute it (Markdown)", "pleaseInputDescription": "Please enter a skill description", @@ -1387,6 +1661,10 @@ "enableChannel": "Enable channel", "enableChannelDesc": "When off, the channel stops receiving and sending messages but keeps its configuration", "displaySettings": "Message display", + "responseMode": "Response mode", + "responseModeDesc": "Final response hides progress narration before tool calls; live progress keeps the current staged messages", + "responseModeInvoke": "Final only (Recommended)", + "responseModeStream": "Live progress", "showToolHints": "Show tool hints", "showToolHintsDesc": "When enabled, channel messages show tool-call activity and status hints", "globalSettings": "Global Settings", @@ -1639,7 +1917,9 @@ "tencent-hai": "Tencent Cloud HAI", "mimo": "Xiaomi MiMo", "minimax": "MiniMax", - "volces": "Volcengine" + "volces": "Volcengine", + "ollama": "Ollama (Local)", + "onnx": "ONNX (Local)" }, "voice": { "loading": "Loading voice settings…", @@ -1699,6 +1979,11 @@ "hitlTools": "Tools requiring approval", "hitlHint": "Listed tools pause before execution until approved or rejected in chat.", "hitlEnableWarning": "Enabling tool approval will pause the agent whenever a listed tool is about to run, requiring manual confirmation. This may add friction: in IM channels you must send /approve or /reject to continue; in the Dashboard an approval card will appear. Make sure you are comfortable with these extra steps.", + "hitlToolsPickerHint": "Check tools that require human approval before execution. The tool id is shown below each label.", + "hitlToolsSelectDefaults": "Recommended defaults", + "hitlToolsSelectAll": "Select all", + "hitlToolsDeselectAll": "Clear all", + "hitlToolsEmpty": "No approvable tools are available.", "fsDesc": "Deny filesystem tools from reading or writing sensitive paths.", "fsEnable": "Enable filesystem path rules", "fsPaths": "Sensitive paths (one per line, glob supported)", @@ -1927,6 +2212,31 @@ "localDeleteFailed": "Failed to delete model", "localDownloadPending": "Preparing to download...", "localDownloading": "Downloading {{repo}}... This may take a few minutes.", + "localDownloadConfirmTitle": "Confirm model download", + "localDownloadConfirmOnnx": "Download {{name}} (about {{size}}). This may take a few minutes.", + "localDownloadConfirmOllama": "Pull {{name}} via Ollama. Size depends on the model and may take a while.", + "localDownloadSizeUnknown": "size unknown", + "localDownloadProgressTitle": "Downloading model", + "localDownloadPreparing": "Preparing download…", + "localDownloadContinueBackground": "Continue in background", + "localDownloadBackground": "Download moved to the background. You will be notified when it finishes.", + "localDownloadBackgroundHint": "You can close this window. The download continues on the server and you will get a notification when it finishes.", + "onnxDownloadProgress": "Downloading {{model}} ({{percent}}%)", + "onnxDownloadLoading": "Loading {{model}}…", + "defaultModelDownloadedOnly": "Only downloaded models can be selected", + "defaultModelNeedDownload": "Download a model below first", + "downloadBeforeEnable": "Download the model before enabling it", + "notDownloaded": "Not downloaded", + "localModelDownloaded": "Downloaded", + "localServiceHint": "Start or stop the local service", + "localServiceLabel": "Service", + "localRuntime": "Local runtime", + "localServiceRunning": "Service running", + "localServiceOn": "Service on", + "localServiceOff": "Service off", + "localServiceStarted": "Local service started", + "localServiceStopped": "Local service stopped", + "localServiceToggleFailed": "Failed to toggle local service", "localCancelDownload": "Cancel Download", "localCancelDownloadConfirm": "Cancel download of \"{{repo}}\"?", "localDownloadCancelled": "Download cancelled", @@ -2070,7 +2380,43 @@ "fetchModelsFailed": "Failed to fetch models: {{error}}", "fetchModelsUnsupportedKind": "Auto-fetch is only supported for OpenAI-compatible providers", "customModelsLabel": "Models", - "customModelsHint": "Fetched models are off by default — enable, test, edit, or remove; or add manually" + "customModelsHint": "Fetched models are off by default — enable, test, edit, or remove; or add manually", + "onnxLocalService": "Local ONNX embedding", + "onnxServiceHint": "Local ONNX embedding model cache (download / enable / probe). Not a chat model; not used by Memory.", + "onnxNoProviderForm": "Local cache only — enable the service, pick a catalog model, download weights. Not chat and not Memory.", + "onnxModelLabel": "Model", + "onnxModelPlaceholder": "Select a catalog model", + "onnxApply": "Apply", + "onnxRecommended": "Recommended", + "onnxDepsPending": "Local embedding components are not ready yet. They install automatically when you turn the service on.", + "onnxDepsInstallFailed": "Automatic installation failed. Check your network connection and try again.", + "onnxInstallingDeps": "Installing ONNX local dependencies — this may take a few minutes…", + "onnxDepsInstalled": "ONNX local dependencies installed", + "enableAfterDownloadFailed": "The model downloaded, but auto-enable failed. Turn it on manually and save.", + "embeddingOnlyTag": "Embedding only (not in chat / Auto)", + "embeddingModel": "Embedding model", + "embeddingModelHint": "When on, this model is only used for knowledge-base retrieval and is hidden from chat / Auto.", + "onnxTestNeedDownload": "Download the model before testing", + "onnxCached": "Cached", + "onnxSelectModel": "Select or enter a model id first", + "onnxEnabled": "ONNX local service enabled", + "onnxDisabled": "ONNX local service disabled", + "onnxModelApplied": "Model set to {{model}}", + "onnxDownloadStarted": "Downloading {{model}}…", + "onnxDownloadDone": "Downloaded {{model}}", + "onnxDownloadFailed": "ONNX model download failed", + "onnxLoadFailed": "Failed to load ONNX service status", + "onnxSaveFailed": "Failed to update ONNX service", + "onnxDeleteFailed": "Failed to delete cached model", + "onnxModelDeleted": "Deleted cached model {{model}}", + "onnxLoading": "Loading {{model}}…", + "onnxDownloading": "Downloading {{model}}…", + "onnxStatusLine": "Status: {{ready}} · model {{model}}", + "onnxReady": "ready", + "onnxNotReady": "not ready", + "onnxCacheDir": "Cache: {{dir}}", + "onnxLocalCached": "Cached models", + "onnxQuickDownload": "Quick download from catalog…" }, "advancedSettings": { "description": "Manage runtime configuration and environment variables.", @@ -2822,9 +3168,9 @@ "installSuccess": "Browser installed successfully", "installSuccessHint": "Browser is ready — you can start a session", "installFailed": "Installation failed", - "installFailedHint": "Retry or run playwright install chromium manually", + "installFailedHint": "Automatic installation failed. Try again. If downloads are slow, set PLAYWRIGHT_DOWNLOAD_HOST to a mirror URL and retry.", "notInstalled": "Chromium not installed", - "notInstalledHint": "Run the auto-installer below or manually run playwright install chromium.", + "notInstalledHint": "No usable browser found. Use the button below to install the bundled Chromium automatically.", "install": "Install browser", "installProgress": "Installing…", "installCancelHint": "Install request cancelled. The server may still be installing — refresh status later.", @@ -3410,7 +3756,26 @@ "submit": "Sign in", "failed": "Login failed", "slideHint": "Slide to the end to verify", - "slideVerified": "Verified" + "slideVerified": "Verified", + "or": "or", + "oidcWith": "Continue with {{name}}", + "oidcStartFailed": "Could not start single sign-on", + "oidcError": { + "denied": "Single sign-on was cancelled.", + "state": "Your single sign-on session expired. Please try again.", + "disabled": "Single sign-on is unavailable.", + "misconfigured": "Single sign-on is misconfigured.", + "invalid_token": "The identity provider returned an invalid token.", + "exchange": "Could not verify your identity-provider login.", + "generic": "Single sign-on failed. Please try again." + }, + "oidcComplete": { + "title": "Completing single sign-on", + "loading": "Signing you in…", + "missingCode": "The single sign-on response is missing a login code.", + "failed": "Could not complete single sign-on", + "backToLogin": "Back to sign in" + } }, "adminUsers": { "newUser": "New user", @@ -3438,6 +3803,8 @@ "colId": "ID", "colUsername": "Username", "colDisplayName": "Display name", + "colEmail": "Email", + "colAuth": "Sign-in methods", "colCreatedAt": "Created", "colRole": "Role", "colLoginLock": "Login lock", @@ -3464,7 +3831,57 @@ "noUsers": "No users yet", "totalUsers": "{{count}} user(s)", "statusEnabled": "Enabled", - "statusDisabled": "Disabled" + "statusDisabled": "Disabled", + "ssoBadge": "SSO", + "passwordBadge": "Password", + "tabLocal": "Local users", + "tabSso": "Single sign-on", + "colPermissions": "Permissions", + "permAll": "All (admin)", + "permAllHint": "Admins automatically have every module permission; no need to select them.", + "permEditHint": "Grant management pages and write access per module", + "permGroupSettings": "Settings", + "permGroupControl": "Control", + "permGroupAdmin": "Admin", + "permCatalogEmpty": "No permission items available", + "permCount": "{{count}} selected", + "createSectionAccount": "Account", + "createSectionAccess": "Role & permissions", + "modalEditTitle": "Edit user {{username}}" + }, + "adminSso": { + "enabled": "Enable single sign-on", + "enabledHint": "Allow users to sign in through the configured OpenID Connect provider.", + "displayName": "Provider display name", + "displayNameRequired": "Enter a provider display name", + "issuer": "Issuer URL", + "issuerHint": "The OpenID Connect issuer URL published by your identity provider.", + "issuerRequired": "Enter a valid issuer URL", + "clientId": "Client ID", + "clientIdRequired": "Enter the client ID", + "clientSecret": "Client secret", + "clientSecretHint": "Optional for public clients.", + "clientSecretConfigured": "A client secret is configured. Leave blank to keep it unchanged.", + "clientSecretPlaceholder": "Leave blank to keep the current secret", + "scopes": "Scopes", + "scopesRequired": "Enter at least one scope", + "dashboardOrigin": "Dashboard origin override", + "dashboardOriginHint": "Optional public dashboard URL used after the identity provider redirects back.", + "dashboardOriginInvalid": "Enter a valid dashboard origin URL", + "redirectUri": "Redirect URI", + "redirectUriHint": "Add this exact callback URL to your identity provider configuration.", + "copy": "Copy", + "copyRedirectUri": "Copy redirect URI", + "copySuccess": "Redirect URI copied", + "copyFailed": "Could not copy redirect URI", + "save": "Save", + "saved": "Single sign-on settings saved", + "saveFailed": "Could not save single sign-on settings", + "loadFailed": "Could not load single sign-on settings", + "testConnection": "Test connection", + "testSuccess": "OIDC connection succeeded", + "testFailed": "OIDC connection test failed", + "testHint": "Save changes before testing a new issuer or client ID." }, "adminAudit": { "loadFailed": "Load failed", @@ -3630,10 +4047,10 @@ "addConnection": "Add {{name}}", "displayName": "Display name", "defaultOpen": "Open by default", - "defaultOpenHint": "When off, tools are injected only if you select this connector in chat.", - "defaultOpenWarning": "When on, tools are included by default in Dashboard, IM, and Cron jobs with no connector picks (extra tokens). Dashboard can opt out per turn; Cron explicit picks override defaults.", + "defaultOpenHint": "Applies only to your account. When off, tools are injected only if you select this connector in chat.", + "defaultOpenWarning": "When on, tools are included by default in your Dashboard, IM, and Cron jobs with no connector picks (extra tokens). Dashboard can opt out per turn; Cron explicit picks override defaults.", "defaultOpenLockedBadge": "Default on", - "defaultOpenLockedHint": "Marked default-open. Auto-injected on IM and Cron when no connectors are picked; Dashboard / Cron with explicit picks follow the selection.", + "defaultOpenLockedHint": "Marked default-open for your account. Auto-injected on IM and Cron when no connectors are picked; Dashboard / Cron with explicit picks follow the selection.", "token": "Access token", "getToken": "Get token", "goToAuthorize": "Authorize", @@ -3852,7 +4269,11 @@ }, "adminUsers": { "title": "User Management", - "subtitle": "Create accounts and manage admin vs standard user access" + "subtitle": "Manage local accounts and single sign-on" + }, + "adminSso": { + "title": "Single sign-on", + "subtitle": "Configure an OpenID Connect identity provider" }, "adminStorage": { "title": "Storage", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 6b411fe3..97ea00e9 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -18,6 +18,7 @@ "view": "查看", "create": "创建", "add": "添加", + "actions": "操作", "upload": "上传", "download": "下载", "refresh": "刷新", @@ -37,8 +38,15 @@ "askOctopHint": "若安装始终失败,可复制日志后询问 Octop,让它帮你排查原因。", "close": "关闭", "contentPlaceholder": "输入内容...", + "emojiPick": "选择 Emoji", + "emojiPickerLabel": "Emoji", + "emojiSearchPlaceholder": "筛选 Emoji", + "emojiNoResults": "没有匹配的 Emoji", "gotIt": "知道了", "unknownError": "未知错误", + "noPermission": "没有权限", + "noPermissionHint": "你没有访问此页面的权限,请联系管理员开通。", + "backToChat": "返回聊天", "viewDetail": "查看详情", "viewMore": "查看更多", "deleteFailed": "删除失败", @@ -61,6 +69,7 @@ "SETUP_REQUIRED": "需要完成初始设置。", "DATABASE_NOT_EMPTY": "目标数据库已有用户。请改用空库,或直接登录现有管理员账户。", "BACKUP_DRIVER_MISMATCH": "该备份与当前数据库引擎不一致(SQLite 与 PostgreSQL 不能互恢)。请将运行时切回备份所用引擎后再恢复,或在当前引擎上重新备份。暂不支持跨引擎恢复。", + "BACKUP_IN_PROGRESS": "已有备份任务正在进行,请稍后再试。", "FORBIDDEN": "没有权限。", "NOT_FOUND": "未找到。", "USER_DISABLED": "账号已禁用。", @@ -79,6 +88,7 @@ "PROVIDER_NAME_TAKEN": "提供商名称已被占用。", "PROVIDER_NOT_VISIBLE": "提供商不可见。", "PROVIDER_REFERENCED": "提供商正在使用中,无法删除。", + "PROVIDER_LOCAL_PROTECTED": "本地提供商无法删除。", "STORAGE_BACKEND_NAME_TAKEN": "存储后端名称已被占用。", "STORAGE_BACKEND_REFERENCED": "存储后端正在使用中,无法删除。", "CHANNEL_KIND_UNSUPPORTED": "不支持的通道类型。", @@ -109,13 +119,25 @@ "SKILL_PACKAGE_NOT_FOUND": "未找到技能包。", "SKILL_PACKAGE_NAME_TAKEN": "技能包 {{name}} 已存在。", "SKILL_PACKAGE_BACKEND_UNSUPPORTED": "技能包目前仅支持「本地完整环境(文件系统+指令执行)」或「本地文件系统(不能执行指令)」,且存储根目录须为 /(POSIX)或 Agent 工作区根目录(Windows 默认)。", + "PUBLISHED_EXPERT_SLUG_TAKEN": "已存在发布专家 slug:{{slug}}。", + "PUBLISHED_EXPERT_ALREADY_EXISTS": "该专家已发布为「{{name}}」,请改为刷新已发布模板。", + "OIDC_BAD_REQUEST": "OIDC 请求无效:{{detail}}", "EXPERT_MARKET_FAILED": "专家市场暂时不可用,请稍后重试。", "SKILLHUB_SSL_FAILED": "SkillHub 请求失败:检测到 Python SSL 错误。macOS 可尝试 `brew reinstall openssl@3`,或检查系统 CA 证书。", "DESKTOP_SESSION_LIMIT": "远程桌面并发连接已达上限(最多 {{limit}} 路),请关闭其他连接后重试。", "DESKTOP_CAPTURE_FAILED": "连续抓屏失败。Linux 服务器请重启虚拟桌面;macOS 请在系统设置中授予屏幕录制与辅助功能权限后重连。", "PLUGIN_INVALID_ARCHIVE": "下载内容不是有效的插件 ZIP。请使用直接的 .zip 下载地址(GitHub 请用 raw.githubusercontent.com 或 “Download/raw” 链接,不要用 /blob/ 页面地址)。", "PLUGIN_INSTALL_FAILED": "下载或安装插件失败:{{reason}}", - "PLUGIN_ALREADY_EXISTS": "插件 {{id}} 已安装。" + "PLUGIN_ALREADY_EXISTS": "插件 {{id}} 已安装。", + "KNOWLEDGE_FEATURE_DISABLED": "管理员尚未启用知识库功能。", + "KNOWLEDGE_PREREQUISITES_FAILED": "知识库的向量模型或依赖尚未就绪。", + "KNOWLEDGE_NOT_FOUND": "未找到知识库或文档。", + "KNOWLEDGE_FORBIDDEN": "您没有此知识库的访问权限。", + "KNOWLEDGE_DOC_LIMIT": "每个知识库最多可包含 100 个文档。", + "KNOWLEDGE_DOC_TOO_LARGE": "文档超过 20 MB 大小限制。", + "KNOWLEDGE_BASE_LIMIT": "每个用户最多可创建 20 个知识库。", + "KNOWLEDGE_UNSUPPORTED_TYPE": "知识库不支持此文档类型。", + "KNOWLEDGE_NAME_TAKEN": "您已有同名知识库。" }, "nav": { "chat": "聊天", @@ -162,12 +184,14 @@ "newVersionBadge": "有新版本", "admin": "管理", "adminUsers": "用户", + "adminSso": "单点登录", "adminStorage": "存储", "adminAudit": "审计日志", "adminAgents": "智能体", "adminAdvanced": "应用设置", "security": "安全防护", - "adminPlugins": "插件管理", + "knowledgeBases": "知识库", + "adminPlugins": "插件", "skillPackages": "技能包" }, "skillPackages": { @@ -189,6 +213,14 @@ "fromSkillHubHint": "将 SkillHub 技能集导入为新的技能包。", "addAsPackage": "添加为技能包", "empty": "暂无技能包", + "emptyGuideTitle": "还没有技能包", + "emptyGuideDesc": "技能包把一组可复用的技能收在一起,挂到专家后即可在对话中使用。", + "emptyGuideStepWhat": "技能是什么", + "emptyGuideStepWhatDetail": "技能是给专家的操作说明书(SKILL.md),说明如何完成一类任务,例如检索资料、填写表格或调用工具。", + "emptyGuideStepHow": "怎么用", + "emptyGuideStepHowDetail": "创建技能包后,手动添加技能或从技能市场导入;再到「专家」中挂载该包,之后对话会自动带上这些技能。", + "emptyGuideStepShare": "共享有什么作用", + "emptyGuideStepShareDetail": "技能包对本实例的登录用户可见。其他人可以挂载使用,只有创建者可以编辑或删除。", "selectPackage": "选择一个技能包以查看其中的技能", "backToList": "返回列表", "noDescription": "暂无描述", @@ -213,7 +245,157 @@ "skillSaved": "技能已保存", "skillDeleted": "技能已删除", "resizeSidebar": "调整技能包列表宽度", - "createdBy": "创建者:{{name}}" + "createdBy": "创建者:{{name}}", + "packageId": "技能包 ID", + "copyPackageId": "点击复制技能包 ID", + "collapseListPanel": "收起列表", + "expandListPanel": "展开列表" + }, + "knowledgeBases": { + "title": "知识库", + "subtitle": "上传文档建立索引,在对话中检索引用;可按需共享给同实例用户。", + "featureTitle": "启用知识库", + "settingsTitle": "知识库设置", + "settingsLead": "实例级开关。启用后需配置向量模型;关闭后全实例不可使用知识库。", + "settingsOpen": "启用知识库", + "manageModels": "管理模型", + "unavailableTitle": "知识库尚未就绪", + "unavailableDescription": "请先在右上角设置中启用知识库,并配置可用的向量模型。", + "unavailableDescriptionNonAdmin": "知识库尚未启用或未配置向量模型,请联系管理员。", + "create": "创建知识库", + "edit": "编辑知识库", + "name": "名称", + "nameRequired": "请输入知识库名称", + "description": "描述", + "empty": "暂无知识库", + "emptyGuideTitle": "还没有知识库", + "emptyGuideDesc": "把文档做成可检索的资料库,对话时引用,也可按需共享给同实例用户。", + "emptyGuideStepWhat": "什么是知识库?", + "emptyGuideStepWhatDetail": "知识库会把上传的文档切分并向量化。对话时专家可以检索引用相关段落,而不必把全文塞进上下文。", + "emptyGuideStepHow": "知识库怎么用", + "emptyGuideStepHowDetail": "创建知识库并上传 md / txt / pdf / docx / pptx。可设为默认启用,或在对话中手动勾选。", + "emptyGuideStepShare": "共享有什么作用", + "emptyGuideStepShareDetail": "共享后实例内登录用户可读、可选用;仅所有者可编辑。默认启用只对创建者生效。", + "enableGuideTitle": "怎么开启知识库功能?", + "enableGuideDesc": "知识库是实例级功能。启用后需配置向量模型,即可创建知识库并在对话中检索文档。", + "enableGuideStepOpen": "打开知识库设置", + "enableGuideStepOpenDetail": "点击下方「知识库设置」,进入实例级开关与向量模型配置。", + "enableGuideStepToggle": "启用知识库", + "enableGuideStepToggleDetail": "打开「启用知识库」开关。关闭后全实例都无法使用知识库。", + "enableGuideStepModel": "配置向量模型", + "enableGuideStepModelDetail": "选择本地 ONNX(需先下载模型)或在线 Embedding 提供商,保存后即可使用。", + "noDescription": "暂无描述", + "selectBase": "选择一个知识库以查看文档", + "backToList": "返回列表", + "createdBy": "创建者:{{name}}", + "baseId": "知识库 ID", + "copyBaseId": "点击复制知识库 ID", + "collapseListPanel": "收起列表", + "expandListPanel": "展开列表", + "documentCount": "{{count}} 个文档", + "documentTotal": "共有 {{count}} 个文档", + "defaultOpen": "是否默认启用", + "defaultOpenBadge": "默认开启", + "defaultOpenHint": "可与「是否共享」同时开启。开启后仅你自己的对话会默认带上此知识库;其他人即使能看到共享库也不会自动带上。关闭后需在对话中手动勾选。", + "defaultOpenWarning": "开启后你自己的聊天默认带上此知识库(消耗更多上下文);可在本轮取消", + "shared": "是否共享", + "sharedBadge": "已共享", + "sharedHint": "可与「是否默认启用」同时开启。共享后实例内所有登录用户可读、可选用;仅所有者可编辑。默认启用只对创建者生效。", + "members": "成员", + "share": "分享", + "noMembers": "当前只有您可以访问此知识库。", + "member": "用户 {{id}} · {{role}}", + "userId": "用户 ID", + "userIdRequired": "请输入用户 ID", + "role": "角色", + "roles": { + "viewer": "只读", + "editor": "可编辑" + }, + "documents": "文档", + "documentLimit": "{{count}} / {{max}} 个文档", + "documentLimitReached": "此知识库已达到 {{count}} 个文档的上限。", + "documentTooLarge": "单个文档不能超过 {{sizeMb}} MB。", + "baseLimitReached": "每个用户最多可创建 {{count}} 个知识库。", + "uploadHint": "支持 md / txt / pdf / docx / pptx,单文件不超过 {{sizeMb}} MB。", + "viewCard": "卡片", + "viewTable": "表格", + "updatedAt": "更新时间", + "rebuildDocument": "重建索引", + "rebuildDocumentConfirm": "重新解析并嵌入此文档?", + "rebuildDocumentSuccess": "已开始重建文档索引", + "rebuildDocumentFailed": "重建文档索引失败", + "previewDocument": "预览", + "previewEmpty": "(无文本内容)", + "previewFailed": "预览文档失败", + "icon": "图标", + "iconPlaceholder": "选择知识库图标", + "iconLabels": { + "book-open": "人文", + "landmark": "历史", + "users": "社科", + "scale": "法律", + "flask-conical": "科学", + "atom": "理科", + "wrench": "技术", + "cpu": "计算机", + "terminal": "工程", + "graduation-cap": "教育", + "briefcase": "商业", + "palette": "艺术", + "languages": "语言" + }, + "upload": "上传文档", + "uploaded": "文档已上传", + "uploadFailed": "上传文档失败", + "emptyDocuments": "暂无文档", + "filename": "文件名", + "status": "状态", + "chunks": "分块数", + "chunkCount": "{{count}} 个分块", + "statuses": { + "pending": "待处理", + "processing": "处理中", + "ready": "已就绪", + "failed": "失败" + }, + "statusesShort": { + "pending": "待索引", + "processing": "索引中", + "ready": "就绪", + "failed": "失败" + }, + "deleteConfirm": "删除此知识库及其所有文档?", + "deleteDocumentConfirm": "删除此文档?", + "loadFailed": "加载知识库失败", + "saveFailed": "保存知识库变更失败", + "deleteFailed": "删除失败", + "created": "知识库已创建", + "updated": "知识库已更新", + "deleted": "知识库已删除", + "resizeSidebar": "调整知识库列表宽度", + "enableTitle": "启用知识库", + "enableDescription": "请选择本地 ONNX 或在线 Embedding 向量模型。切换模型会重建全部知识库索引。", + "selectModel": "选择向量模型", + "selectProvider": "选择在线提供商", + "noProviders": "暂无已配置的提供商,请先在模型管理中添加", + "noModels": "暂无可用的向量模型", + "localOnnx": "本地 ONNX", + "remoteEmbedding": "在线 Embedding", + "rebuildConfirmTitle": "重建知识库索引?", + "rebuildConfirmDescription": "切换向量模型会清空并重新嵌入所有知识库索引。", + "notDownloaded": "未下载", + "approxSize": "约 {{size}}", + "sizeUnknown": "大小未知", + "recommended": "推荐", + "showMoreOnnx": "展开更多", + "downloadModel": "下载", + "downloadNeedModel": "所选模型尚未下载,请先下载后再保存。", + "onnxServiceEnabled": "已开启本地 ONNX 服务", + "enable": "启用", + "featureEnabled": "知识库已启用", + "featureDisabled": "知识库已停用", + "featureSaveFailed": "更新知识库功能失败" }, "setupWizard": { "title": "设置向导", @@ -375,6 +557,45 @@ "subtitle": "基于已配置的场景模板,一键创建预置身份/灵魂/心跳的智能体。每个专家自带 system prompt,新建后再为它绑定 provider 并启动即可。", "loadFailed": "加载专家失败", "loadDetailFailed": "加载详情失败", + "share": { + "toggle": "共享给其他用户", + "badge": "已共享", + "fromOwner": "共享专家 · 来自 {{name}}" + }, + "published": { + "title": "发布模板", + "badge": "已发布", + "cardPublish": "发布模板", + "publishConfirmTitle": "发布为模板?", + "publishConfirm": "将把当前专家打成快照模板。其他用户可在「专家库 → 用户发布」中安装为自己的私有副本;不会共享你的对话记录。", + "statusPublished": "此 Agent 已发布为社区模板。", + "statusNotPublished": "发布此 Agent,让其他用户可以安装私有副本。", + "publish": "发布", + "publishSuccess": "模板已发布", + "publishSuccessHint": "模板已发布。其他用户可在「专家库 → 用户发布」中安装。", + "publishFailed": "发布模板失败", + "update": "更新已发布模板", + "updateSuccess": "已更新发布模板", + "updateFailed": "更新发布模板失败", + "unpublish": "取消发布", + "unpublishConfirm": "确定取消发布此模板?已安装的私有副本不会受影响。", + "unpublishSuccess": "模板已取消发布", + "unpublishFailed": "取消发布模板失败", + "listTitle": "用户发布({{count}})", + "listHint": "由本实例用户发布的模板。点击「安装」可为自己创建一份私有专家。", + "emptyHint": "还没有用户发布的模板。在「我的专家」卡片上点击上传图标即可发布。", + "by": "来自 {{name}}", + "install": "安装", + "installTitle": "安装「{{name}}」", + "installFailed": "安装模板失败", + "drawerTitle": "发布专家模板", + "drawerHint": "仅打包专家配置 Markdown(SOUL / IDENTITY 等)、Skills 与子 Agent 定义;对话记忆、上传文件、内置 Skills 等运行时数据不会包含。", + "fieldName": "模板名称", + "fieldDescription": "模板描述", + "fieldWelcomeZh": "中文引导语", + "fieldWelcomeEn": "英文引导语", + "fieldWelcomePlaceholder": "聊天欢迎页展示的一行引导语" + }, "noExperts": "暂无可用专家", "selectExpertHint": "请从左侧选择一个专家以查看详情", "creating": "创建中…", @@ -473,6 +694,8 @@ "basicInfo": "基本信息", "agentName": "名称", "agentDescription": "描述", + "color": "配色", + "colorHint": "选择专家卡片配色,用于列表与聊天头像等展示。", "defaultModel": "默认模型", "defaultModelAuto": "Auto(使用 Provider 默认模型)", "editFile": "编辑 →", @@ -596,6 +819,7 @@ "skillFilesHint": "该专家自带的技能,创建后会写入工作区的 skills/ 目录。", "noSkillFiles": "该专家暂无自带技能", "subagentFilesTitle": "子智能体列表 ({{count}})", + "subagentTemplateHint": "该专家自带的子智能体,创建后会写入工作区的 agents/ 目录。", "subagentFilesHint": "工作区 agents/ 目录下的子智能体定义,主智能体通过 task 工具委派任务。", "noSubagentFiles": "工作区中暂无子智能体", "manageSubagents": "管理子智能体", @@ -644,8 +868,14 @@ "pleaseInputDescription": "请输入描述", "emojiLabel": "Emoji", "emojiPlaceholder": "🤖", - "colorLabel": "颜色", + "emojiHint": "从列表中选择子智能体图标。", + "emojiPick": "选择 Emoji", + "emojiPickerLabel": "Emoji", + "emojiSearchPlaceholder": "筛选 Emoji", + "emojiNoResults": "没有匹配的 Emoji", + "colorLabel": "配色", "colorPlaceholder": "可选,例如 cyan 或 #6366f1", + "colorHint": "选择子智能体卡片配色,用于已安装列表等展示。", "bodyLabel": "指令内容", "bodyPlaceholder": "用 Markdown 编写子智能体的人格与指令…", "pleaseInputBody": "请输入指令内容", @@ -689,6 +919,9 @@ "model_call_failed": "模型调用多次重试后仍失败。请稍后重试;若持续失败,请检查模型配置或更换模型。" }, "chat": { + "sharedExpert": { + "banner": "共享专家 · 由 {{name}} 提供" + }, "thinking": "正在思考", "continuing": "继续生成中", "generating": "生成中", @@ -699,6 +932,11 @@ "refreshingMessages": "正在刷新消息…", "streamResumed": "连接已恢复。若内容不完整,可下拉刷新。", "regenerate": "重新生成", + "forkFromHere": "从此处分叉", + "forkSuccess": "已创建分叉对话", + "forkSuccessEmpty": "已创建新的空对话,请编辑问题后发送", + "forkFailed": "创建分叉失败", + "forkDisabledWhileBusy": "请等待当前回复或审批完成后再分叉", "retry": "重试", "like": "有帮助", "dislike": "没帮助", @@ -764,6 +1002,10 @@ "title": "专家详情" }, "composerMore": "更多工具", + "knowledgePicker": "知识库", + "knowledgePickerSearch": "搜索知识库", + "knowledgePickerEmpty": "暂无可用知识库", + "manageKnowledgeBases": "管理知识库", "skillPicker": "选择技能", "skillPickerSearch": "搜索技能", "skillPickerEmpty": "暂无可用技能", @@ -822,7 +1064,8 @@ "read_file": "读取文件", "write_file": "写入文件", "edit_file": "编辑文件", - "execute": "执行", + "execute": "执行指令", + "bash": "Shell 命令 (bash)", "current_time": "当前时间", "write_todos": "编写计划", "task": "子智能体任务", @@ -845,6 +1088,7 @@ "cronjob_update": "更新定时任务", "cronjob_delete": "删除定时任务", "cronjob_run_now": "立即执行定时任务", + "search_knowledge": "检索知识库", "agent_list": "列出 Agent", "ask_agent": "咨询 Agent", "call_agent": "调用 Agent", @@ -974,7 +1218,27 @@ "importConfirmOk": "恢复", "importSuccess": "恢复完成({{agents}} 个 Agent,{{files}} 个工作区文件)", "importFailed": "恢复备份失败", - "restoreConfig": "同时恢复 config.json 与 env(需手动重启服务后生效)" + "restoreConfig": "同时恢复 config.json 与 env(需手动重启服务后生效)", + "autoTitle": "自动备份", + "autoDesc": "按计划将全量系统备份写入备份目录;保留策略只清理自动备份,不影响手动备份。", + "autoEnabled": "启用自动备份", + "autoSchedule": "备份周期", + "autoScheduleDaily": "每天 04:00", + "autoScheduleWeekly": "每周日 04:00", + "autoSchedule12h": "每 12 小时", + "autoScheduleCustom": "自定义", + "autoScheduleHint": "cron:分 时 日 月 周(服务器时区);interval:秒数,例如 interval:43200 表示每 12 小时。", + "autoIntervalPreview": "当前:每 {{seconds}} 秒(约 {{hours}} 小时)", + "autoRetention": "保留最近 N 份自动备份", + "autoSave": "保存设置", + "autoSaveSuccess": "自动备份设置已保存", + "autoSaveFailed": "保存自动备份设置失败", + "autoLoadFailed": "加载自动备份设置失败", + "autoRunNow": "立即备份", + "autoRunSuccess": "已创建自动备份", + "autoRunFailed": "立即备份失败", + "autoScheduled": "调度:已生效", + "autoNotScheduled": "调度:未生效" }, "skills": { "title": "技能", @@ -992,6 +1256,8 @@ "invalidSkillUrlSource": "请输入有效的 HTTP(S) 技能 URL;具体来源由服务端适配器校验", "zipHintTitle": "压缩包结构(每个一级文件夹 = 一个技能):", "zipHintDetail": "每个文件夹内必须包含 SKILL.md,可附带脚本等其他文件。仅支持 .zip。", + "zipDragDropHint": "将需要上传的文件拖拽到此处", + "zipSelected": "已选择: {{name}}", "chooseZip": "选择 ZIP 文件", "noZipSelected": "未选择文件", "removeZip": "移除", @@ -1012,6 +1278,12 @@ "createSkill": "创建技能", "viewSkill": "查看技能", "editSkill": "编辑技能", + "fileTreeTitle": "技能文件", + "fileTreeEmpty": "暂无文件", + "fileTreeShow": "展开技能文件", + "fileTreeHide": "收起技能文件", + "fileTreeAgentNotReady": "智能体未运行,无法加载技能文件", + "finishEditBeforeSwitchFile": "请先保存或取消编辑,再切换文件", "saveSkill": "保存", "viewPreview": "预览", "viewSource": "源码", @@ -1100,6 +1372,8 @@ "addMetadata": "添加字段", "metadataKeyRequired": "请输入 Key", "metadataValueRequired": "请输入 Value", + "emojiLabel": "Emoji", + "emojiHint": "从列表中选择技能卡片图标。", "bodyLabel": "技能实现内容", "bodyPlaceholder": "说明技能的用途、触发条件与执行步骤(Markdown)", "pleaseInputDescription": "请输入技能描述", @@ -1385,6 +1659,10 @@ "enableChannel": "启用频道", "enableChannelDesc": "关闭后频道将停止接收和发送消息,但保留配置", "displaySettings": "消息展示", + "responseMode": "回复方式", + "responseModeDesc": "仅发送最终回复会隐藏工具调用前的过程说明;实时过程保留当前逐阶段消息", + "responseModeInvoke": "仅最终回复(推荐)", + "responseModeStream": "实时过程", "showToolHints": "显示工具调用提示", "showToolHintsDesc": "开启后,频道消息中将展示工具调用过程与状态提示", "globalSettings": "全局设置", @@ -1638,7 +1916,9 @@ "tencent-hai": "腾讯云 HAI", "mimo": "小米 MiMo", "minimax": "MiniMax", - "volces": "火山引擎" + "volces": "火山引擎", + "ollama": "Ollama(本地)", + "onnx": "ONNX(本地)" }, "voice": { "loading": "加载语音配置…", @@ -1697,6 +1977,11 @@ "hitlTools": "需审批的工具", "hitlHint": "命中列表的工具在执行前会暂停,等待用户在对话中批准或拒绝。", "hitlEnableWarning": "开启工具审批后,Agent 执行特定工具时将暂停并等待确认,这可能带来一些操作不便:在 IM 频道中需手动发送 /approve 或 /reject 指令才能继续;在 Dashboard 对话中会弹出审批卡片。请确认您愿意承担这些额外操作步骤。", + "hitlToolsPickerHint": "勾选需要在执行前人工审批的工具,下方为工具 id。", + "hitlToolsSelectDefaults": "推荐默认项", + "hitlToolsSelectAll": "全选", + "hitlToolsDeselectAll": "清空", + "hitlToolsEmpty": "暂无可配置审批的工具。", "fsDesc": "禁止文件工具读写敏感路径。", "fsEnable": "启用文件路径规则", "fsPaths": "敏感路径(每行一条,支持 glob)", @@ -1924,6 +2209,31 @@ "localDeleteFailed": "删除模型失败", "localDownloadPending": "准备下载...", "localDownloading": "正在下载 {{repo}}... 可能需要几分钟。", + "localDownloadConfirmTitle": "确认下载模型", + "localDownloadConfirmOnnx": "即将下载 {{name}}(约 {{size}})。下载可能需要几分钟,请保持网络畅通。", + "localDownloadConfirmOllama": "即将通过 Ollama 拉取 {{name}}。下载体积取决于模型,可能需要较长时间。", + "localDownloadSizeUnknown": "大小未知", + "localDownloadProgressTitle": "正在下载模型", + "localDownloadPreparing": "准备下载…", + "localDownloadContinueBackground": "后台继续", + "localDownloadBackground": "下载已转到后台,完成后会通知你", + "localDownloadBackgroundHint": "可关闭此窗口,下载在服务端继续,完成后会弹出通知。", + "onnxDownloadProgress": "正在下载 {{model}}({{percent}}%)", + "onnxDownloadLoading": "正在加载 {{model}}…", + "defaultModelDownloadedOnly": "仅可选择已下载的模型", + "defaultModelNeedDownload": "请先在下方管理模型中下载", + "downloadBeforeEnable": "请先下载模型后再启用", + "notDownloaded": "未下载", + "localModelDownloaded": "已下载", + "localServiceHint": "控制本地服务是否启动", + "localServiceLabel": "服务", + "localRuntime": "本地运行时", + "localServiceRunning": "服务运行中", + "localServiceOn": "服务已开启", + "localServiceOff": "服务已关闭", + "localServiceStarted": "本地服务已启动", + "localServiceStopped": "本地服务已停止", + "localServiceToggleFailed": "切换本地服务失败", "localCancelDownload": "取消下载", "localCancelDownloadConfirm": "确定取消下载 \"{{repo}}\"?", "localDownloadCancelled": "下载已取消", @@ -2066,7 +2376,43 @@ "fetchModelsFailed": "获取模型失败:{{error}}", "fetchModelsUnsupportedKind": "仅 OpenAI 兼容供应商支持自动获取模型列表", "customModelsLabel": "模型", - "customModelsHint": "获取后默认未启用,可开启开关、测试、编辑或删除;也可手动添加" + "customModelsHint": "获取后默认未启用,可开启开关、测试、编辑或删除;也可手动添加", + "onnxLocalService": "本地 ONNX Embedding", + "onnxServiceHint": "本机 ONNX Embedding 模型缓存(下载 / 启用 / 探测)。不是对话模型,也不会接入 Memory。", + "onnxNoProviderForm": "仅本地缓存:打开开关、从目录选模型并下载权重。不用于对话,也不用于 Memory。", + "onnxModelLabel": "模型", + "onnxModelPlaceholder": "从目录选择模型", + "onnxApply": "应用", + "onnxRecommended": "推荐", + "onnxDepsPending": "本地嵌入组件尚未就绪,开启服务时将自动安装。", + "onnxDepsInstallFailed": "自动安装失败,请检查网络连接后重试。", + "onnxInstallingDeps": "正在安装 ONNX 本地依赖,可能需要几分钟…", + "onnxDepsInstalled": "ONNX 本地依赖已安装", + "enableAfterDownloadFailed": "模型已下载,但自动启用失败,请手动开启后保存。", + "embeddingOnlyTag": "仅 Embedding(不进聊天/Auto)", + "embeddingModel": "向量模型", + "embeddingModelHint": "开启后仅用于知识库等向量检索,不会出现在聊天与 Auto 列表中。", + "onnxTestNeedDownload": "请先下载模型后再测试", + "onnxCached": "已缓存", + "onnxSelectModel": "请先选择或输入模型 ID", + "onnxEnabled": "已开启本地 ONNX 服务", + "onnxDisabled": "已关闭本地 ONNX 服务", + "onnxModelApplied": "已选择模型 {{model}}", + "onnxDownloadStarted": "正在下载 {{model}}…", + "onnxDownloadDone": "已下载 {{model}}", + "onnxDownloadFailed": "ONNX 模型下载失败", + "onnxLoadFailed": "加载 ONNX 服务状态失败", + "onnxSaveFailed": "更新 ONNX 服务失败", + "onnxDeleteFailed": "删除本地缓存失败", + "onnxModelDeleted": "已删除缓存模型 {{model}}", + "onnxLoading": "正在加载 {{model}}…", + "onnxDownloading": "正在下载 {{model}}…", + "onnxStatusLine": "状态:{{ready}} · 模型 {{model}}", + "onnxReady": "就绪", + "onnxNotReady": "未就绪", + "onnxCacheDir": "缓存目录:{{dir}}", + "onnxLocalCached": "已缓存模型", + "onnxQuickDownload": "从目录快速下载…" }, "advancedSettings": { "description": "管理运行配置和环境变量等高级选项。", @@ -2966,9 +3312,9 @@ "installSuccess": "浏览器安装成功", "installSuccessHint": "浏览器已就绪,可启动会话", "installFailed": "安装失败", - "installFailedHint": "可重试或手动运行 playwright install chromium", + "installFailedHint": "自动安装失败,请重试。若网络不稳定,可配置 PLAYWRIGHT_DOWNLOAD_HOST 镜像源后再次安装。", "notInstalled": "Chromium 未安装", - "notInstalledHint": "运行自动安装或手动执行 playwright install chromium。", + "notInstalledHint": "尚未检测到可用浏览器,请点击下方按钮自动安装内置 Chromium。", "install": "安装浏览器", "installProgress": "正在安装中…", "installCancelHint": "已取消安装请求,服务端可能仍在继续安装,请稍后刷新状态。", @@ -3554,7 +3900,26 @@ "submit": "登录", "failed": "登录失败", "slideHint": "拖动滑块到最右侧完成验证", - "slideVerified": "验证通过" + "slideVerified": "验证通过", + "or": "或", + "oidcWith": "使用 {{name}} 继续", + "oidcStartFailed": "无法启动单点登录", + "oidcError": { + "denied": "已取消单点登录。", + "state": "单点登录会话已过期,请重试。", + "disabled": "单点登录不可用。", + "misconfigured": "单点登录配置有误。", + "invalid_token": "身份提供商返回了无效令牌。", + "exchange": "无法验证身份提供商登录。", + "generic": "单点登录失败,请重试。" + }, + "oidcComplete": { + "title": "正在完成单点登录", + "loading": "正在登录…", + "missingCode": "单点登录响应缺少登录代码。", + "failed": "无法完成单点登录", + "backToLogin": "返回登录" + } }, "adminUsers": { "newUser": "新建用户", @@ -3582,6 +3947,8 @@ "colId": "ID", "colUsername": "用户名", "colDisplayName": "显示名称", + "colEmail": "邮箱", + "colAuth": "登录方式", "colCreatedAt": "创建时间", "colRole": "角色", "colLoginLock": "登录锁定", @@ -3608,7 +3975,57 @@ "noUsers": "暂无用户", "totalUsers": "共 {{count}} 个用户", "statusEnabled": "已启用", - "statusDisabled": "已禁用" + "statusDisabled": "已禁用", + "ssoBadge": "单点登录", + "passwordBadge": "密码", + "tabLocal": "内置用户", + "tabSso": "单点登录", + "colPermissions": "权限", + "permAll": "全部(管理员)", + "permAllHint": "管理员自动拥有全部模块权限,无需单独勾选。", + "permEditHint": "按模块授予管理页面与写入权限", + "permGroupSettings": "设置", + "permGroupControl": "控制", + "permGroupAdmin": "管理", + "permCatalogEmpty": "暂无可用权限项", + "permCount": "{{count}} 项", + "createSectionAccount": "账号信息", + "createSectionAccess": "角色与权限", + "modalEditTitle": "编辑用户 {{username}}" + }, + "adminSso": { + "enabled": "启用单点登录", + "enabledHint": "允许用户通过已配置的 OpenID Connect 身份提供商登录。", + "displayName": "提供商显示名称", + "displayNameRequired": "请输入提供商显示名称", + "issuer": "签发者 URL", + "issuerHint": "身份提供商公开的 OpenID Connect 签发者 URL。", + "issuerRequired": "请输入有效的签发者 URL", + "clientId": "客户端 ID", + "clientIdRequired": "请输入客户端 ID", + "clientSecret": "客户端密钥", + "clientSecretHint": "公共客户端可不填写。", + "clientSecretConfigured": "已配置客户端密钥;留空将保留现有密钥。", + "clientSecretPlaceholder": "留空以保留现有密钥", + "scopes": "授权范围", + "scopesRequired": "请至少输入一个授权范围", + "dashboardOrigin": "控制台来源地址覆盖", + "dashboardOriginHint": "可选。身份提供商回调后使用的公开控制台 URL。", + "dashboardOriginInvalid": "请输入有效的控制台来源地址 URL", + "redirectUri": "回调地址", + "redirectUriHint": "请将此精确回调 URL 添加到身份提供商配置中。", + "copy": "复制", + "copyRedirectUri": "复制回调地址", + "copySuccess": "回调地址已复制", + "copyFailed": "无法复制回调地址", + "save": "保存", + "saved": "单点登录设置已保存", + "saveFailed": "无法保存单点登录设置", + "loadFailed": "无法加载单点登录设置", + "testConnection": "测试连接", + "testSuccess": "OIDC 连接成功", + "testFailed": "OIDC 连接测试失败", + "testHint": "更改签发者或客户端 ID 后,请先保存再测试。" }, "adminAudit": { "loadFailed": "加载失败", @@ -3774,10 +4191,10 @@ "addConnection": "添加 {{name}}", "displayName": "显示名称", "defaultOpen": "是否默认打开", - "defaultOpenHint": "关闭时需在对话中手动勾选才会注入工具。", - "defaultOpenWarning": "开启后默认会在 Dashboard、IM 与 Cron(未特殊选连接器时)携带该工具(额外消耗 token)。Dashboard 可关本轮;Cron 若显式选择连接器则以选择为准。", + "defaultOpenHint": "仅对你自己的账号生效:关闭时需在对话中手动勾选才会注入工具。", + "defaultOpenWarning": "开启后默认会在你的 Dashboard、IM 与 Cron(未特殊选连接器时)携带该工具(额外消耗 token)。Dashboard 可关本轮;Cron 若显式选择连接器则以选择为准。", "defaultOpenLockedBadge": "默认打开", - "defaultOpenLockedHint": "连接器已设为默认打开。IM 与未选手动连接器的 Cron 会自动携带;Dashboard / 已选手动的 Cron 以用户选择为准。", + "defaultOpenLockedHint": "连接器已设为默认打开(仅你自己的对话)。IM 与未选手动连接器的 Cron 会自动携带;Dashboard / 已选手动的 Cron 以选择为准。", "token": "访问 Token", "getToken": "获取 Token", "goToAuthorize": "前往授权", @@ -3987,7 +4404,7 @@ "subtitle": "Agent 的本地文件系统工作空间" }, "models": { - "title": "模型管理", + "title": "模型配置", "subtitle": "配置 LLM 提供商和 API 密钥" }, "voice": { @@ -3996,7 +4413,11 @@ }, "adminUsers": { "title": "用户管理", - "subtitle": "创建与管理账户,并区分管理员与普通用户的权限范围" + "subtitle": "管理内置账号与单点登录" + }, + "adminSso": { + "title": "单点登录", + "subtitle": "配置 OpenID Connect 身份提供商" }, "adminStorage": { "title": "存储管理", diff --git a/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx b/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx new file mode 100644 index 00000000..93cbc50a --- /dev/null +++ b/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { I18nextProvider } from "react-i18next"; +import i18n from "../../../i18n"; + +const { getOidcConfig, putOidcConfig, testOidcConfig } = vi.hoisted(() => ({ + getOidcConfig: vi.fn(), + putOidcConfig: vi.fn(), + testOidcConfig: vi.fn(), +})); + +vi.mock("../../../api/modules/sso", () => ({ + ssoApi: { getOidcConfig, putOidcConfig, testOidcConfig }, +})); + +vi.mock("@/utils/antdMessage", () => ({ + message: { error: vi.fn(), success: vi.fn() }, +})); + +import SsoPanel from "./SsoPanel"; + +describe("", () => { + it("loads the provider configuration and displays its callback URL", async () => { + getOidcConfig.mockResolvedValue({ + enabled: true, + display_name: "Acme SSO", + issuer: "https://identity.example.com", + client_id: "octop", + scopes: "openid profile email", + dashboard_origin: "https://octop.example.com", + has_client_secret: true, + redirect_uri: "https://octop.example.com/api/auth/oidc/callback", + }); + + render( + + + , + ); + + await waitFor(() => expect(getOidcConfig).toHaveBeenCalledOnce()); + expect(screen.getByDisplayValue("Acme SSO")).toBeInTheDocument(); + expect( + screen.getByDisplayValue( + "https://octop.example.com/api/auth/oidc/callback", + ), + ).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/pages/Admin/Users/SsoPanel.tsx b/dashboard/src/pages/Admin/Users/SsoPanel.tsx new file mode 100644 index 00000000..cb1006c5 --- /dev/null +++ b/dashboard/src/pages/Admin/Users/SsoPanel.tsx @@ -0,0 +1,230 @@ +import { useCallback, useEffect, useState } from "react"; +import { Button, Form, Input, Space, Spin, Switch, Typography } from "antd"; +import { Copy, FlaskConical, Save } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { message } from "@/utils/antdMessage"; +import { + ssoApi, + type OidcConfig, + type OidcConfigPut, +} from "../../../api/modules/sso"; +import { apiErrorMessage } from "../../../utils/apiError"; + +interface SsoFormValues { + enabled: boolean; + display_name: string; + issuer: string; + client_id: string; + client_secret?: string; + scopes: string; + dashboard_origin?: string; +} + +function configToFormValues(config: OidcConfig): SsoFormValues { + return { + enabled: config.enabled, + display_name: config.display_name, + issuer: config.issuer, + client_id: config.client_id, + scopes: config.scopes, + dashboard_origin: config.dashboard_origin ?? "", + }; +} + +export default function SsoPanel() { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [redirectUri, setRedirectUri] = useState(""); + const [hasClientSecret, setHasClientSecret] = useState(false); + + const loadConfig = useCallback(async () => { + setLoading(true); + try { + const config = await ssoApi.getOidcConfig(); + form.setFieldsValue(configToFormValues(config)); + setRedirectUri(config.redirect_uri ?? ""); + setHasClientSecret(config.has_client_secret); + } catch (error) { + message.error(apiErrorMessage(error, t("adminSso.loadFailed"), t)); + } finally { + setLoading(false); + } + }, [form, t]); + + useEffect(() => { + void loadConfig(); + }, [loadConfig]); + + const saveConfig = async (values: SsoFormValues) => { + setSaving(true); + try { + const body: OidcConfigPut = { + ...values, + client_secret: values.client_secret?.trim() || undefined, + dashboard_origin: values.dashboard_origin?.trim() || null, + }; + const saved = await ssoApi.putOidcConfig(body); + form.setFieldsValue(configToFormValues(saved)); + form.setFieldValue("client_secret", undefined); + setHasClientSecret(saved.has_client_secret); + if (saved.redirect_uri) { + setRedirectUri(saved.redirect_uri); + } + message.success(t("adminSso.saved")); + } catch (error) { + message.error(apiErrorMessage(error, t("adminSso.saveFailed"), t)); + } finally { + setSaving(false); + } + }; + + const testConnection = async () => { + setTesting(true); + try { + const result = await ssoApi.testOidcConfig(); + if (result.ok) { + message.success(result.detail || t("adminSso.testSuccess")); + } else { + message.error(result.detail || t("adminSso.testFailed")); + } + } catch (error) { + message.error(apiErrorMessage(error, t("adminSso.testFailed"), t)); + } finally { + setTesting(false); + } + }; + + const copyRedirectUri = async () => { + try { + await navigator.clipboard.writeText(redirectUri); + message.success(t("adminSso.copySuccess")); + } catch { + message.error(t("adminSso.copyFailed")); + } + }; + + return ( + + + form={form} + layout="vertical" + requiredMark={false} + onFinish={(values) => void saveConfig(values)} + style={{ maxWidth: 680 }} + initialValues={{ enabled: false, scopes: "openid profile email" }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {t("adminSso.testHint")} + + + + ); +} diff --git a/dashboard/src/pages/Admin/Users/UsersListPanel.tsx b/dashboard/src/pages/Admin/Users/UsersListPanel.tsx new file mode 100644 index 00000000..f77ab732 --- /dev/null +++ b/dashboard/src/pages/Admin/Users/UsersListPanel.tsx @@ -0,0 +1,1715 @@ +/** + * Admin → Users page (plan §14.7). + * + * List all users with role/disabled toggles, password reset, delete. + * Card and table views; default is card on mobile, table on desktop. The view switcher + refresh + + * new-user buttons live in a content-area toolbar (mirrors the Experts + * page layout). Each row/card shows agent count; click opens a drawer + * with that user's agents. + * + * Endpoints (all require admin role; backend returns 403 otherwise): + * GET /api/users + * POST /api/users + * PATCH /api/users/{id} + * POST /api/users/{id}/reset-password + * DELETE /api/users/{id} + */ + +import { useEffect, useMemo, useState, useCallback } from "react"; +import { + Table, + Button, + Modal, + Form, + Input, + Space, + Popconfirm, + Switch, + Typography, + Tooltip, + Drawer, + Empty, + Spin, + Tag, + Segmented, + Checkbox, +} from "antd"; +import { message } from "@/utils/antdMessage"; + +import { + Bot, + Check, + ChevronRight, + Clock, + IdCard, + KeyRound, + LayoutGrid, + List, + Lock, + LockOpen, + Pencil, + Plus, + RefreshCw, + Search, + ShieldCheck, + Trash2, + User, + UserRound, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { request } from "../../../api/request"; +import { authApi } from "../../../api/modules/auth"; +import { useCardTableView } from "../../../hooks/useCardTableView"; +import { useServerTimezone } from "../../../hooks/useServerTimezone"; +import { formatServerDateTime } from "../../../utils/formatMessageTime"; +import type { OctopAgent } from "../../../context/AgentContext"; +import { AgentCard } from "../../Experts/components/AgentCard"; +import EditAgentDrawer from "../../Experts/components/EditAgentDrawer"; +import expertStyles from "../../Experts/index.module.less"; +import styles from "./index.module.less"; + +const { Text } = Typography; + +interface UserRow { + id: number; + username: string; + role: "admin" | "user"; + display_name: string | null; + email?: string | null; + has_password?: boolean; + sso_linked?: boolean; + disabled: boolean; + login_failed_count?: number; + login_locked?: boolean; + login_locked_until?: number; + login_retry_after_seconds?: number; + created_at?: number; + permissions?: string[]; +} + +interface PermissionCatalogItem { + key: string; + category: string; + label: string; + page?: string; + page_label?: string; +} + +function permFullLabel(item: PermissionCatalogItem): string { + if (item.page_label) return `${item.page_label} / ${item.label}`; + return item.label; +} + +interface CreateValues { + username: string; + display_name?: string; + password: string; + confirm: string; + role: "admin" | "user"; + permissions?: string[]; +} + +interface EditValues { + role: "admin" | "user"; + permissions?: string[]; +} + +interface ResetValues { + password: string; + confirm: string; +} + +function roleToneClass(role: "admin" | "user"): string { + return role === "admin" ? styles.roleToneAdmin : styles.roleToneUser; +} + +function useNowSeconds(active: boolean): number { + const [now, setNow] = useState(() => Math.floor(Date.now() / 1000)); + useEffect(() => { + if (!active) return; + const id = window.setInterval( + () => setNow(Math.floor(Date.now() / 1000)), + 1000, + ); + return () => window.clearInterval(id); + }, [active]); + return now; +} + +function lockRemainingSeconds(row: UserRow, nowSec: number): number { + if (!row.login_locked || !row.login_locked_until) return 0; + return Math.max(0, row.login_locked_until - nowSec); +} + +function formatUserTs(ts: number | undefined, timeZone: string): string { + if (!ts) return "—"; + return formatServerDateTime(ts, timeZone); +} + +interface UserCardGridProps { + rows: UserRow[]; + loading: boolean; + agentsByUserId: Map; + agentsLoading: boolean; + currentUserId: number | null; + permLabelByKey: Map; + onTogglePatch: ( + row: UserRow, + patch: Partial>, + ) => Promise; + onEdit: (row: UserRow) => void; + onShowAgents: (row: UserRow) => void; + onResetPassword: (row: UserRow) => void; + onDelete: (row: UserRow) => Promise; + onUnlockLogin: (row: UserRow) => Promise; + nowSec: number; +} + +function userInitials(displayName: string, username: string): string { + const source = displayName.trim() || username; + const parts = source.split(/[\s._-]+/).filter(Boolean); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return source.slice(0, 2).toUpperCase(); +} + +const FIELD_ICON_PROPS = { + size: 16 as const, + style: { color: "var(--fn-text-tertiary)" }, +}; + +interface RolePickerProps { + value?: "admin" | "user"; + onChange?: (value: "admin" | "user") => void; + disabled?: boolean; + options: { + value: "admin" | "user"; + label: string; + hint: string; + }[]; +} + +function RolePicker({ value, onChange, options, disabled }: RolePickerProps) { + return ( +
+ {options.map((opt) => { + const selected = value === opt.value; + const Icon = opt.value === "admin" ? ShieldCheck : UserRound; + return ( + + ); + })} +
+ ); +} + +interface PermissionCheckboxPickerProps { + value?: string[]; + onChange?: (value: string[]) => void; + catalog: PermissionCatalogItem[]; + disabled?: boolean; +} + +function PermissionCheckboxPicker({ + value, + onChange, + catalog, + disabled, +}: PermissionCheckboxPickerProps) { + const { t } = useTranslation(); + const selected = value ?? []; + const selectedSet = useMemo(() => new Set(selected), [selected]); + + const groups = useMemo(() => { + const order = [ + { + category: "settings", + label: t("adminUsers.permGroupSettings"), + }, + { + category: "control", + label: t("adminUsers.permGroupControl"), + }, + { + category: "admin", + label: t("adminUsers.permGroupAdmin"), + }, + ] as const; + return order + .map((g) => { + const items = catalog.filter((p) => p.category === g.category); + const pages: { + page: string; + label: string; + items: PermissionCatalogItem[]; + }[] = []; + const standalone: PermissionCatalogItem[] = []; + for (const item of items) { + if (!item.page) { + standalone.push(item); + continue; + } + const existing = pages.find((p) => p.page === item.page); + if (existing) { + existing.items.push(item); + } else { + pages.push({ + page: item.page, + label: item.page_label || item.page, + items: [item], + }); + } + } + return { ...g, items, standalone, pages }; + }) + .filter((g) => g.items.length > 0); + }, [catalog, t]); + + const toggle = (key: string, checked: boolean) => { + if (disabled) return; + if (checked) { + onChange?.([...selected, key]); + return; + } + onChange?.(selected.filter((k) => k !== key)); + }; + + const setGroup = (keys: string[], checked: boolean) => { + if (disabled) return; + if (checked) { + const next = new Set(selected); + for (const k of keys) next.add(k); + onChange?.(Array.from(next)); + return; + } + const drop = new Set(keys); + onChange?.(selected.filter((k) => !drop.has(k))); + }; + + if (catalog.length === 0) { + return ( +
+ {t("adminUsers.permCatalogEmpty")} +
+ ); + } + + return ( +
+ {groups.map((group) => { + const keys = group.items.map((i) => i.key); + const checkedCount = keys.filter((k) => selectedSet.has(k)).length; + const allChecked = checkedCount === keys.length && keys.length > 0; + const indeterminate = checkedCount > 0 && !allChecked; + const renderChips = (items: PermissionCatalogItem[]) => ( +
+ {items.map((item) => { + const checked = selectedSet.has(item.key); + return ( + + ); + })} +
+ ); + return ( +
+
+ setGroup(keys, e.target.checked)} + > + {group.label} + + + {checkedCount}/{keys.length} + +
+ {group.pages.length === 0 ? ( + renderChips(group.items) + ) : ( + <> + {group.standalone.length > 0 + ? renderChips(group.standalone) + : null} + {group.pages.map((page) => { + const pageKeys = page.items.map((i) => i.key); + const pageChecked = pageKeys.filter((k) => + selectedSet.has(k), + ).length; + const pageAll = + pageChecked === pageKeys.length && pageKeys.length > 0; + const pageIndeterminate = pageChecked > 0 && !pageAll; + return ( +
+
+ setGroup(pageKeys, e.target.checked)} + > + + {page.label} + + + + {pageChecked}/{pageKeys.length} + +
+ {renderChips(page.items)} +
+ ); + })} + + )} +
+ ); + })} +
+ ); +} + +function PermissionSummary({ + row, + permLabelByKey, +}: { + row: UserRow; + permLabelByKey: Map; +}) { + const { t } = useTranslation(); + if (row.role === "admin") { + return ( + + {t("adminUsers.permAll")} + + ); + } + const keys = row.permissions ?? []; + if (keys.length === 0) { + return ; + } + const names = keys.map((key) => permLabelByKey.get(key) ?? key); + return ( + + + {t("adminUsers.permCount", { count: keys.length })} + + + ); +} + +function RoleLegend() { + const { t } = useTranslation(); + return ( +
+ + {t("adminUsers.roleLegendTitle")} + +

{t("adminUsers.roleLegend")}

+
+ ); +} + +function UserCardGrid({ + rows, + loading, + agentsByUserId, + agentsLoading, + currentUserId, + permLabelByKey, + onTogglePatch, + onEdit, + onShowAgents, + onResetPassword, + onDelete, + onUnlockLogin, + nowSec, +}: UserCardGridProps) { + const { t } = useTranslation(); + const timeZone = useServerTimezone(); + if (loading && rows.length === 0) { + return ( +
+ +
+ ); + } + if (rows.length === 0) { + return ; + } + return ( +
+ {rows.map((row) => { + const agentCount = agentsByUserId.get(row.id)?.length ?? 0; + const isSelf = row.id === currentUserId; + const displayName = row.display_name?.trim() || row.username; + const remaining = lockRemainingSeconds(row, nowSec); + const isLocked = remaining > 0; + const failedCount = row.login_failed_count ?? 0; + const accentClass = isLocked + ? styles.userCardAccentLocked + : row.disabled + ? styles.userCardAccentDisabled + : row.role === "admin" + ? styles.userCardAccentAdmin + : styles.userCardAccentUser; + const statusColor = row.disabled ? "#8c8c8c" : "#52c41a"; + const statusBg = row.disabled + ? "rgba(140,140,140,0.10)" + : "rgba(82,196,26,0.10)"; + return ( +
+
+ +
+
+ + +
+
+ {displayName} + {isSelf && ( + + {t("adminUsers.you")} + + )} +
+
@{row.username}
+
+ + + void onTogglePatch(row, { disabled: !checked }) + } + className={styles.userCardSwitch} + aria-label={t("common.enabled")} + /> +
+ +
+ + {row.role === "admin" + ? t("adminUsers.roleAdmin") + : t("adminUsers.roleUser")} + + + + {row.disabled + ? t("adminUsers.statusDisabled") + : t("adminUsers.statusEnabled")} + + {row.sso_linked && ( + + {t("adminUsers.ssoBadge")} + + )} + {row.has_password && ( + + {t("adminUsers.passwordBadge")} + + )} +
+ +
+ {row.created_at != null && ( + + + + {formatUserTs(row.created_at, timeZone)} + + + )} + +
+ +
+ +
+ + {isLocked && ( +
+ + + {t("adminUsers.loginLockActive", { + minutes: Math.max(1, Math.ceil(remaining / 60)), + })} + + +
+ )} + + {!isLocked && failedCount > 0 && ( +
+ {t("adminUsers.loginFailedCount", { count: failedCount })} +
+ )} + +
+ + + + + + + + + void onDelete(row)} + disabled={isSelf} + > + + + + + + + + #{row.id} +
+
+
+ ); + })} +
+ ); +} + +/** + * Compact login-lock status indicator. Used by both the card view + * (inline in a `userCard2Row`) and the table view (table cell). + */ +function UserLoginLock({ + row, + nowSec, + onUnlock, +}: { + row: UserRow; + nowSec: number; + onUnlock: () => void; +}) { + const { t } = useTranslation(); + const failedCount = row.login_failed_count ?? 0; + if (!row.login_locked) { + if (failedCount > 0) { + return ( + + {t("adminUsers.loginFailedCount", { count: failedCount })} + + ); + } + return ( + + {t("adminUsers.loginLockNone")} + + ); + } + const remaining = lockRemainingSeconds(row, nowSec); + const minutes = Math.max(1, Math.ceil(remaining / 60)); + return ( + + + {t("adminUsers.loginLockActive", { minutes })} + + + + ); +} + +export default function UsersListPanel() { + const { t } = useTranslation(); + const timeZone = useServerTimezone(); + const [agents, setAgents] = useState([]); + const [agentsLoading, setAgentsLoading] = useState(true); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + const [createOpen, setCreateOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [form] = Form.useForm(); + const [editTarget, setEditTarget] = useState(null); + const [editSubmitting, setEditSubmitting] = useState(false); + const [editForm] = Form.useForm(); + const [resetTarget, setResetTarget] = useState(null); + const [resetSubmitting, setResetSubmitting] = useState(false); + const [resetForm] = Form.useForm(); + const [currentUserId, setCurrentUserId] = useState(null); + const [agentDrawerUser, setAgentDrawerUser] = useState(null); + const [editAgent, setEditAgent] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const { viewMode, setViewMode, showCardView } = useCardTableView("table"); + const [permCatalog, setPermCatalog] = useState([]); + + const permLabelByKey = useMemo(() => { + const map = new Map(); + for (const item of permCatalog) { + map.set(item.key, permFullLabel(item)); + } + return map; + }, [permCatalog]); + + const baselinePermissions = useMemo( + () => + permCatalog.filter((p) => p.category === "settings").map((p) => p.key), + [permCatalog], + ); + + const createRoleOptions = useMemo( + () => [ + { + value: "user" as const, + label: t("adminUsers.roleUser"), + hint: t("adminUsers.roleUserHint"), + }, + { + value: "admin" as const, + label: t("adminUsers.roleAdmin"), + hint: t("adminUsers.roleAdminHint"), + }, + ], + [t], + ); + + const isSelfAdmin = useCallback( + (row: UserRow) => row.id === currentUserId && row.role === "admin", + [currentUserId], + ); + + const hasLockedUser = useMemo( + () => rows.some((row) => row.login_locked), + [rows], + ); + const nowSec = useNowSeconds(hasLockedUser); + + const agentsByUserId = useMemo(() => { + const map = new Map(); + for (const agent of agents) { + if (agent.user_id == null) continue; + const list = map.get(agent.user_id) ?? []; + list.push(agent); + map.set(agent.user_id, list); + } + return map; + }, [agents]); + + const drawerAgents = agentDrawerUser + ? agentsByUserId.get(agentDrawerUser.id) ?? [] + : []; + + const filteredRows = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + if (!query) return rows; + return rows.filter((row) => { + const username = row.username.toLowerCase(); + const displayName = (row.display_name ?? "").trim().toLowerCase(); + return username.includes(query) || displayName.includes(query); + }); + }, [rows, searchQuery]); + + const refreshUsers = useCallback(async () => { + setLoading(true); + try { + const data = await request("/users"); + setRows(data); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.loadFailed"), + ); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + if (!hasLockedUser) return; + const anyExpired = rows.some( + (row) => row.login_locked && lockRemainingSeconds(row, nowSec) === 0, + ); + if (anyExpired) void refreshUsers(); + }, [hasLockedUser, nowSec, rows, refreshUsers]); + + const refreshAgents = useCallback(async () => { + setAgentsLoading(true); + try { + const data = await request("/agents?scope=all"); + setAgents(data); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.loadFailed"), + ); + setAgents([]); + } finally { + setAgentsLoading(false); + } + }, [t]); + + const patchAgent = useCallback( + (agentId: string, patch: Partial) => { + setAgents((prev) => + prev.map((a) => (a.agent_id === agentId ? { ...a, ...patch } : a)), + ); + }, + [], + ); + + const handleDrawerStateChange = useCallback( + (agentId: string, newState: string) => { + patchAgent(agentId, { state: newState }); + }, + [patchAgent], + ); + + const handleDrawerDeleted = useCallback( + (agentId: string) => { + setAgents((prev) => prev.filter((a) => a.agent_id !== agentId)); + void refreshAgents(); + }, + [refreshAgents], + ); + + const handleEditSaved = useCallback( + ( + updated: Pick< + OctopAgent, + "agent_id" | "name" | "description" | "default_model" + >, + ) => { + setEditAgent(null); + patchAgent(updated.agent_id, { + name: updated.name, + description: updated.description, + default_model: updated.default_model, + }); + void refreshAgents(); + }, + [patchAgent, refreshAgents], + ); + + const refreshAll = useCallback(async () => { + await Promise.all([refreshUsers(), refreshAgents()]); + }, [refreshUsers, refreshAgents]); + + useEffect(() => { + void refreshAll(); + authApi + .me() + .then((u) => setCurrentUserId(u.id)) + .catch(() => setCurrentUserId(null)); + request("/users/permissions") + .then(setPermCatalog) + .catch(() => setPermCatalog([])); + }, [refreshAll]); + + const onCreate = async (values: CreateValues) => { + setSubmitting(true); + try { + await request("/users", { + method: "POST", + body: JSON.stringify({ + username: values.username, + display_name: values.display_name?.trim() || null, + password: values.password, + role: values.role, + permissions: values.role === "admin" ? [] : values.permissions ?? [], + }), + }); + message.success( + t("adminUsers.createSuccess", { username: values.username }), + ); + form.resetFields(); + setCreateOpen(false); + void refreshUsers(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.createFailed"), + ); + } finally { + setSubmitting(false); + } + }; + + const openCreate = () => { + form.setFieldsValue({ + role: "user", + permissions: [...baselinePermissions], + username: undefined, + display_name: undefined, + password: undefined, + confirm: undefined, + }); + setCreateOpen(true); + }; + + const openEdit = (row: UserRow) => { + setEditTarget(row); + editForm.setFieldsValue({ + role: row.role, + permissions: [...(row.permissions ?? [])], + }); + }; + + const togglePatch = async ( + row: UserRow, + patch: Partial>, + ): Promise => { + if ( + patch.role === "user" && + row.id === currentUserId && + row.role === "admin" + ) { + message.warning(t("adminUsers.demoteSelf")); + return false; + } + try { + await request(`/users/${row.id}`, { + method: "PATCH", + body: JSON.stringify(patch), + }); + void refreshUsers(); + return true; + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.updateFailed"), + ); + return false; + } + }; + + const onEditSubmit = async (values: EditValues) => { + if (!editTarget) return; + setEditSubmitting(true); + try { + const ok = await togglePatch(editTarget, { + role: values.role, + permissions: values.role === "admin" ? [] : values.permissions ?? [], + }); + if (ok) { + setEditTarget(null); + editForm.resetFields(); + } + } finally { + setEditSubmitting(false); + } + }; + + const onDelete = async (row: UserRow) => { + try { + await request(`/users/${row.id}`, { method: "DELETE" }); + message.success(t("adminUsers.deleteSuccess")); + void refreshAll(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("common.deleteFailed"), + ); + } + }; + + const onResetSubmit = async (values: ResetValues) => { + if (!resetTarget) return; + setResetSubmitting(true); + try { + await request(`/users/${resetTarget.id}/reset-password`, { + method: "POST", + body: JSON.stringify({ new_password: values.password }), + }); + message.success(t("adminUsers.resetSuccess")); + setResetTarget(null); + resetForm.resetFields(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.resetFailed"), + ); + } finally { + setResetSubmitting(false); + } + }; + + const onUnlockLogin = async (row: UserRow) => { + try { + await request(`/users/${row.id}/unlock-login`, { method: "POST" }); + message.success(t("adminUsers.unlockLoginSuccess")); + void refreshUsers(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("adminUsers.unlockLoginFailed"), + ); + } + }; + + return ( + <> +
+ +
+ } + value={searchQuery} + onChange={(event) => setSearchQuery(event.target.value)} + placeholder={t("adminUsers.searchPlaceholder")} + className={styles.userSearch} + /> +
+ setViewMode(v as "table" | "card")} + options={[ + { + value: "card", + label: ( + + + {t("adminUsers.viewCard", "卡片")} + + ), + }, + { + value: "table", + label: ( + + + {t("adminUsers.viewTable", "表格")} + + ), + }, + ]} + /> + + +
+
+
+ + {showCardView ? ( + { + setResetTarget(row); + resetForm.resetFields(); + }} + onDelete={onDelete} + onUnlockLogin={onUnlockLogin} + nowSec={nowSec} + /> + ) : ( + + rowKey="id" + size="middle" + className={styles.userTable} + loading={loading} + dataSource={filteredRows} + pagination={false} + scroll={{ x: 960 }} + rowClassName={(row) => + [ + row.disabled ? styles.userTableRowDisabled : "", + row.login_locked ? styles.userTableRowLocked : "", + ] + .filter(Boolean) + .join(" ") + } + columns={[ + { + title: t("adminUsers.colUsername"), + width: 240, + render: (_, row) => { + const displayName = row.display_name?.trim() || row.username; + return ( +
+ + {userInitials(displayName, row.username)} + + + + {displayName} + {row.id === currentUserId && ( + + {t("adminUsers.you")} + + )} + + + @{row.username} + + +
+ ); + }, + }, + { + title: t("adminUsers.colAuth"), + width: 120, + render: (_, row) => { + const parts = [ + row.sso_linked ? t("adminUsers.ssoBadge") : null, + row.has_password ? t("adminUsers.passwordBadge") : null, + ].filter(Boolean); + return ( + + {parts.length ? parts.join(" · ") : "—"} + + ); + }, + }, + { + title: t("adminUsers.colAgents"), + width: 80, + render: (_, row) => { + const count = agentsByUserId.get(row.id)?.length ?? 0; + return ( + + ); + }, + }, + { + title: t("adminUsers.colRole"), + width: 88, + render: (_, row) => ( + + {row.role === "admin" + ? t("adminUsers.roleAdmin") + : t("adminUsers.roleUser")} + + ), + }, + { + title: t("adminUsers.colPermissions"), + width: 120, + render: (_, row) => ( + + ), + }, + { + title: t("common.enabled"), + width: 72, + render: (_, row) => ( + + togglePatch(row, { disabled: !checked }) + } + /> + ), + }, + { + title: t("adminUsers.colCreatedAt"), + dataIndex: "created_at", + width: 156, + render: (ts: number | undefined) => ( + + {formatUserTs(ts, timeZone)} + + ), + }, + { + title: t("adminUsers.colLoginLock"), + width: 180, + render: (_, row) => ( + void onUnlockLogin(row)} + /> + ), + }, + { + title: t("adminUsers.colActions"), + width: 120, + render: (_, row) => ( + + + + + + + + onDelete(row)} + disabled={row.id === currentUserId} + > + + + + + + ), + }, + ]} + /> + )} + + setAgentDrawerUser(null)} + width={400} + destroyOnHidden + > + + {drawerAgents.length === 0 ? ( + + ) : ( +
+ {drawerAgents.map((agent) => ( + + setEditAgent( + drawerAgents.find((a) => a.agent_id === id) ?? null, + ) + } + onDeleted={handleDrawerDeleted} + onStateChange={handleDrawerStateChange} + onPollSettled={() => void refreshAgents()} + /> + ))} +
+ )} +
+
+ + setEditAgent(null)} + onSaved={handleEditSaved} + /> + + { + setCreateOpen(false); + form.resetFields(); + }} + width={Math.min( + 520, + typeof window !== "undefined" ? window.innerWidth - 24 : 520, + )} + destroyOnHidden + className={styles.createUserDrawer} + styles={{ body: { paddingTop: 12, paddingBottom: 24 } }} + footer={ +
+ + +
+ } + > + + form={form} + layout="vertical" + requiredMark={false} + onFinish={onCreate} + initialValues={{ role: "user", permissions: [] }} + className={styles.createUserForm} + > +
+
+ {t("adminUsers.createSectionAccount")} +
+ + } autoFocus /> + + + } /> + + + } + autoComplete="new-password" + /> + + ({ + validator(_, value) { + if (!value || getFieldValue("password") === value) { + return Promise.resolve(); + } + return Promise.reject( + new Error(t("wizard.admin.passwordMismatch")), + ); + }, + }), + ]} + > + } + autoComplete="new-password" + /> + +
+ +
+
+ {t("adminUsers.createSectionAccess")} +
+ + + + prev.role !== cur.role} + > + {({ getFieldValue }) => { + const isAdminRole = getFieldValue("role") === "admin"; + if (isAdminRole) { + return ( +
+ + {t("adminUsers.permAllHint")} +
+ ); + } + return ( + + + + ); + }} +
+
+ +
+ + { + setEditTarget(null); + editForm.resetFields(); + }} + width={Math.min( + 520, + typeof window !== "undefined" ? window.innerWidth - 24 : 520, + )} + destroyOnHidden + className={styles.createUserDrawer} + styles={{ body: { paddingTop: 12, paddingBottom: 24 } }} + footer={ +
+ + +
+ } + > + + form={editForm} + layout="vertical" + requiredMark={false} + onFinish={onEditSubmit} + className={styles.createUserForm} + > +
+
+ {t("adminUsers.createSectionAccess")} +
+ + + + prev.role !== cur.role} + > + {({ getFieldValue }) => { + const isAdminRole = getFieldValue("role") === "admin"; + if (isAdminRole) { + return ( +
+ + {t("adminUsers.permAllHint")} +
+ ); + } + return ( + + + + ); + }} +
+
+ +
+ + { + setResetTarget(null); + resetForm.resetFields(); + }} + onOk={() => resetForm.submit()} + okText={t("common.reset")} + cancelText={t("common.cancel")} + confirmLoading={resetSubmitting} + > + + {t("adminUsers.resetHint")} + + + form={resetForm} + layout="vertical" + onFinish={onResetSubmit} + > + + + } + /> + + ({ + validator(_, value) { + if (!value || getFieldValue("password") === value) { + return Promise.resolve(); + } + return Promise.reject( + new Error(t("wizard.admin.passwordMismatch")), + ); + }, + }), + ]} + > + + } + /> + + + + + ); +} diff --git a/dashboard/src/pages/Admin/Users/index.module.less b/dashboard/src/pages/Admin/Users/index.module.less index ef69192a..04c24bdd 100644 --- a/dashboard/src/pages/Admin/Users/index.module.less +++ b/dashboard/src/pages/Admin/Users/index.module.less @@ -23,12 +23,24 @@ padding: 60px; } -/* ── Create-user modal ──────────────────────────────────────────── */ +/* ── Create-user drawer ─────────────────────────────────────────── */ -.createUserModal { - :global(.ant-modal-body) { - padding-top: 12px; +.createUserDrawer { + :global(.ant-drawer-header) { + border-bottom: 1px solid var(--fn-border-primary); } + + :global(.ant-drawer-footer) { + border-top: 1px solid var(--fn-border-primary); + padding: 12px 16px; + } +} + +.createUserFooter { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; } .createUserForm { @@ -42,8 +54,195 @@ } } +.createSection { + margin-bottom: 8px; + + & + & { + margin-top: 8px; + padding-top: 16px; + border-top: 1px solid var(--fn-border-primary); + } +} + +.createSectionTitle { + margin-bottom: 12px; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--fn-text-tertiary); + text-transform: uppercase; +} + .createUserRoleItem { - margin-bottom: 4px !important; + margin-bottom: 12px !important; +} + +.createUserPermItem { + margin-bottom: 0 !important; +} + +.permAdminHint { + display: flex; + align-items: flex-start; + gap: 8px; + margin-top: 4px; + padding: 10px 12px; + border-radius: var(--fn-radius-md, 8px); + background: var(--fn-color-brand-bg); + border: 1px solid + var( + --fn-color-brand-border, + color-mix(in srgb, var(--fn-color-brand) 22%, transparent) + ); + color: var(--fn-text-secondary); + font-size: 13px; + line-height: 1.45; + + svg { + flex-shrink: 0; + margin-top: 1px; + color: var(--fn-color-brand); + } +} + +.permEmpty { + padding: 12px 0; +} + +.permPicker { + display: flex; + flex-direction: column; + gap: 14px; +} + +.permPickerDisabled { + opacity: 0.55; + pointer-events: none; +} + +.permGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.permGroupHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 28px; +} + +.permGroupTitle { + font-size: 13px; + font-weight: 600; + color: var(--fn-text-primary); +} + +.permGroupCount { + font-size: 12px; + font-variant-numeric: tabular-nums; + color: var(--fn-text-tertiary); +} + +.permPage { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 10px 10px; + border-radius: var(--fn-radius-md, 8px); + background: var(--fn-bg-secondary, var(--fn-bg-elevated)); +} + +.permPageHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.permPageTitle { + font-size: 12px; + font-weight: 600; + color: var(--fn-text-secondary); +} + +.permGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + + @media (max-width: 480px) { + grid-template-columns: 1fr; + } +} + +.permChip { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 36px; + padding: 6px 10px; + text-align: left; + border: 1px solid var(--fn-border-primary); + border-radius: var(--fn-radius-md, 8px); + background: var(--fn-bg-elevated, var(--fn-bg-primary)); + color: var(--fn-text-primary); + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; + + &:hover:not(:disabled) { + border-color: var(--fn-border-strong, var(--fn-border-primary)); + background: var(--fn-bg-secondary, var(--fn-bg-elevated)); + } + + &:disabled { + cursor: not-allowed; + } +} + +.permChipSelected { + border-color: var( + --fn-color-brand-border, + color-mix(in srgb, var(--fn-color-brand) 45%, transparent) + ); + background: var( + --fn-color-brand-bg, + color-mix(in srgb, var(--fn-color-brand) 8%, transparent) + ); + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--fn-color-brand) 18%, transparent); + color: var(--fn-color-brand); +} + +.permChipCheck { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--fn-border-primary); + background: var(--fn-bg-primary); + color: var(--fn-color-on-brand, #fff); +} + +.permChipSelected .permChipCheck { + border-color: var(--fn-color-brand); + background: var(--fn-color-brand); +} + +.permChipLabel { + flex: 1; + min-width: 0; + font-size: 13px; + line-height: 1.3; } .rolePicker { @@ -58,6 +257,7 @@ } .roleOption { + --role-accent: var(--fn-text-secondary); display: flex; align-items: flex-start; gap: 8px; @@ -80,6 +280,11 @@ background: var(--fn-bg-secondary, var(--fn-bg-elevated)); } + &:disabled { + cursor: not-allowed; + opacity: 0.55; + } + &:focus-visible { outline: 2px solid var(--fn-color-brand); outline-offset: 2px; @@ -87,30 +292,43 @@ } .roleOptionSelected { - box-shadow: 0 0 0 1px var(--role-accent); - border-color: var(--role-accent); - background: color-mix( - in srgb, - var(--role-accent) 10%, - var(--fn-bg-elevated, var(--fn-bg-primary)) + border-color: var( + --fn-color-brand-border, + color-mix(in srgb, var(--fn-color-brand) 45%, transparent) + ); + background: var( + --fn-color-brand-bg, + color-mix(in srgb, var(--fn-color-brand) 8%, transparent) ); + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--fn-color-brand) 18%, transparent); &:hover { - border-color: var(--role-accent); - background: color-mix( - in srgb, - var(--role-accent) 14%, - var(--fn-bg-elevated, var(--fn-bg-primary)) + border-color: var(--fn-color-brand); + background: var( + --fn-color-brand-bg, + color-mix(in srgb, var(--fn-color-brand) 8%, transparent) ); } + + .roleOptionIcon { + color: var(--fn-color-brand); + background: color-mix(in srgb, var(--fn-color-brand) 14%, transparent); + } + + .roleOptionLabel { + color: var(--fn-color-brand); + } } -.roleOptionAdmin { - --role-accent: #d4880e; +.roleToneAdmin { + color: var(--fn-color-brand); + background: var(--fn-color-brand-bg); } -.roleOptionUser { - --role-accent: #4f6ef7; +.roleToneUser { + color: var(--fn-text-secondary); + background: var(--fn-bg-container, rgba(0, 0, 0, 0.04)); } .roleOptionIcon { @@ -237,12 +455,28 @@ flex-shrink: 0; } +.userCardAccentAdmin { + background: var(--fn-color-brand); +} + +.userCardAccentUser { + background: var(--fn-border-strong, var(--fn-border-primary)); +} + +.userCardAccentLocked { + background: #ff4d4f; +} + +.userCardAccentDisabled { + background: var(--fn-text-tertiary); +} + .userCardInner { display: flex; flex-direction: column; flex: 1; padding: 16px 16px 12px; - gap: 14px; + gap: 12px; } /* ── Header ─────────────────────────────────────────────────────── */ @@ -298,6 +532,9 @@ line-height: 16px !important; padding: 0 5px !important; border-radius: 4px !important; + border: none !important; + color: var(--fn-color-brand) !important; + background: var(--fn-color-brand-bg) !important; } .userCardHandle { @@ -314,14 +551,10 @@ display: inline-flex; align-items: center; gap: 4px; - margin-top: 4px; - font-size: 11px; + font-size: 12px; color: var(--fn-text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; } .userCardMeta { @@ -329,7 +562,47 @@ align-items: center; flex-wrap: wrap; gap: 6px; - margin-top: 8px; +} + +.userCardInfo { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.userCardAuth { + font-size: 11px; + color: var(--fn-text-tertiary); + padding: 1px 7px; + border-radius: 999px; + background: var(--fn-bg-container, rgba(0, 0, 0, 0.04)); +} + +.permBadge, +.permBadgeMuted { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + font-size: 12px; + line-height: 1; + white-space: nowrap; +} + +.permBadge { + color: var(--fn-text-secondary); + background: var(--fn-bg-container, rgba(0, 0, 0, 0.04)); +} + +.permBadgeAll { + color: var(--fn-color-brand); + background: var(--fn-color-brand-bg); +} + +.permBadgeMuted { + color: var(--fn-text-tertiary); } .userCardPill { @@ -519,3 +792,110 @@ border-radius: var(--fn-radius-sm); background: var(--fn-bg-container, rgba(0, 0, 0, 0.03)); } + +.userTable { + :global(.ant-table) { + background: transparent; + } + + :global(.ant-table-container) { + border: 1px solid var(--fn-border-primary); + border-radius: var(--fn-radius-lg, 12px); + overflow: hidden; + } + + :global(.ant-table-thead > tr > th) { + background: var(--fn-bg-secondary, var(--fn-bg-elevated)); + color: var(--fn-text-tertiary); + font-size: 12px; + font-weight: 600; + padding: 10px 12px; + } + + :global(.ant-table-tbody > tr > td) { + padding: 10px 12px; + vertical-align: middle; + } +} + +.userTableRowDisabled { + opacity: 0.72; +} + +.userTableRowLocked :global(td) { + background: rgba(255, 77, 79, 0.03); +} + +.userCell { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.userCellAvatar { + width: 32px; + height: 32px; + border-radius: 9px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 11px; + font-weight: 700; +} + +.userCellText { + display: flex; + flex-direction: column; + min-width: 0; + gap: 1px; +} + +.userCellName { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--fn-text-primary); + line-height: 1.3; +} + +.userCellYou { + font-size: 10px; + font-weight: 600; + color: var(--fn-color-brand); + background: var(--fn-color-brand-bg); + border-radius: 4px; + padding: 0 5px; + line-height: 16px; +} + +.userCellHandle { + font-size: 12px; + color: var(--fn-text-tertiary); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.userCellMuted { + font-size: 12px; + color: var(--fn-text-tertiary); +} + +.userCellLink { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + border: 0; + background: none; + color: var(--fn-text-secondary); + cursor: pointer; + font-size: 13px; + font-variant-numeric: tabular-nums; + + &:hover { + color: var(--fn-text-primary); + } +} diff --git a/dashboard/src/pages/Admin/Users/index.tsx b/dashboard/src/pages/Admin/Users/index.tsx index bccf1353..42c66e95 100644 --- a/dashboard/src/pages/Admin/Users/index.tsx +++ b/dashboard/src/pages/Admin/Users/index.tsx @@ -1,1187 +1,58 @@ -/** - * Admin → Users page (plan §14.7). - * - * List all users with role/disabled toggles, password reset, delete. - * Card and table views; default is card on mobile, table on desktop. The view switcher + refresh + - * new-user buttons live in a content-area toolbar (mirrors the Experts - * page layout). Each row/card shows agent count; click opens a drawer - * with that user's agents. - * - * Endpoints (all require admin role; backend returns 403 otherwise): - * GET /api/users - * POST /api/users - * PATCH /api/users/{id} - * POST /api/users/{id}/reset-password - * DELETE /api/users/{id} - */ - -import { useEffect, useMemo, useState, useCallback } from "react"; -import { - Table, - Button, - Modal, - Form, - Input, - Select, - Space, - Popconfirm, - Switch, - Typography, - Tooltip, - Drawer, - Empty, - Spin, - Tag, - Segmented, -} from "antd"; -import { message } from "@/utils/antdMessage"; - -import { - Bot, - ChevronRight, - Clock, - IdCard, - KeyRound, - LayoutGrid, - List, - Lock, - LockOpen, - Plus, - RefreshCw, - Search, - ShieldCheck, - Trash2, - User, - UserRound, -} from "lucide-react"; import { useTranslation } from "react-i18next"; +import { KeyRound, Users } from "lucide-react"; +import type { ReactNode } from "react"; import PageShell from "../../../layouts/PageShell"; -import { request } from "../../../api/request"; -import { authApi } from "../../../api/modules/auth"; -import { useCardTableView } from "../../../hooks/useCardTableView"; -import { useServerTimezone } from "../../../hooks/useServerTimezone"; -import { formatServerDateTime } from "../../../utils/formatMessageTime"; -import type { OctopAgent } from "../../../context/AgentContext"; -import { AgentCard } from "../../Experts/components/AgentCard"; -import EditAgentDrawer from "../../Experts/components/EditAgentDrawer"; -import expertStyles from "../../Experts/index.module.less"; -import styles from "./index.module.less"; - -const { Text } = Typography; - -interface UserRow { - id: number; - username: string; - role: "admin" | "user"; - display_name: string | null; - disabled: boolean; - login_failed_count?: number; - login_locked?: boolean; - login_locked_until?: number; - login_retry_after_seconds?: number; - created_at?: number; -} - -interface CreateValues { - username: string; - display_name?: string; - password: string; - confirm: string; - role: "admin" | "user"; -} - -interface ResetValues { - password: string; - confirm: string; -} - -interface RoleMeta { - color: string; - bg: string; -} - -const ROLE_META: Record<"admin" | "user", RoleMeta> = { - admin: { color: "#d4880e", bg: "rgba(212,136,14,0.10)" }, - user: { color: "#1677ff", bg: "rgba(22,119,255,0.10)" }, -}; - -function useNowSeconds(active: boolean): number { - const [now, setNow] = useState(() => Math.floor(Date.now() / 1000)); - useEffect(() => { - if (!active) return; - const id = window.setInterval( - () => setNow(Math.floor(Date.now() / 1000)), - 1000, - ); - return () => window.clearInterval(id); - }, [active]); - return now; -} - -function lockRemainingSeconds(row: UserRow, nowSec: number): number { - if (!row.login_locked || !row.login_locked_until) return 0; - return Math.max(0, row.login_locked_until - nowSec); -} - -function formatUserTs(ts: number | undefined, timeZone: string): string { - if (!ts) return "—"; - return formatServerDateTime(ts, timeZone); -} - -interface UserCardGridProps { - rows: UserRow[]; - loading: boolean; - agentsByUserId: Map; - agentsLoading: boolean; - currentUserId: number | null; - roleOptions: { value: "admin" | "user"; label: string }[]; - onTogglePatch: ( - row: UserRow, - patch: Partial>, - ) => Promise; - onShowAgents: (row: UserRow) => void; - onResetPassword: (row: UserRow) => void; - onDelete: (row: UserRow) => Promise; - onUnlockLogin: (row: UserRow) => Promise; - nowSec: number; - isSelfAdmin: (row: UserRow) => boolean; -} - -function userInitials(displayName: string, username: string): string { - const source = displayName.trim() || username; - const parts = source.split(/[\s._-]+/).filter(Boolean); - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase(); - } - return source.slice(0, 2).toUpperCase(); -} - -const FIELD_ICON_PROPS = { - size: 16 as const, - style: { color: "var(--fn-text-tertiary)" }, -}; - -interface RolePickerProps { - value?: "admin" | "user"; - onChange?: (value: "admin" | "user") => void; - options: { - value: "admin" | "user"; - label: string; - hint: string; - }[]; -} - -function RolePicker({ value, onChange, options }: RolePickerProps) { - return ( -
- {options.map((opt) => { - const selected = value === opt.value; - const Icon = opt.value === "admin" ? ShieldCheck : UserRound; - return ( - - ); - })} -
- ); -} - -function RoleLegend() { - const { t } = useTranslation(); - return ( -
- - {t("adminUsers.roleLegendTitle")} - -

{t("adminUsers.roleLegend")}

-
- ); -} - -function UserCardGrid({ - rows, - loading, - agentsByUserId, - agentsLoading, - currentUserId, - roleOptions, - onTogglePatch, - onShowAgents, - onResetPassword, - onDelete, - onUnlockLogin, - nowSec, - isSelfAdmin, -}: UserCardGridProps) { - const { t } = useTranslation(); - const timeZone = useServerTimezone(); - if (loading && rows.length === 0) { - return ( -
- -
- ); - } - if (rows.length === 0) { - return ; - } - return ( -
- {rows.map((row) => { - const agentCount = agentsByUserId.get(row.id)?.length ?? 0; - const isSelf = row.id === currentUserId; - const displayName = row.display_name?.trim() || row.username; - const remaining = lockRemainingSeconds(row, nowSec); - const isLocked = remaining > 0; - const roleMeta = ROLE_META[row.role] ?? ROLE_META.user; - const failedCount = row.login_failed_count ?? 0; - const accentColor = isLocked - ? "#ff4d4f" - : row.disabled - ? "#8c8c8c" - : roleMeta.color; - const statusColor = row.disabled ? "#8c8c8c" : "#52c41a"; - const statusBg = row.disabled - ? "rgba(140,140,140,0.10)" - : "rgba(82,196,26,0.10)"; - return ( -
-
- -
-
- - -
-
- {displayName} - {isSelf && ( - - {t("adminUsers.you")} - - )} -
-
@{row.username}
- {row.created_at != null && ( - -
- - {formatUserTs(row.created_at, timeZone)} -
-
- )} -
- - } - value={searchQuery} - onChange={(event) => setSearchQuery(event.target.value)} - placeholder={t("adminUsers.searchPlaceholder")} - className={styles.userSearch} - /> -
- setViewMode(v as "table" | "card")} - options={[ - { - value: "card", - label: ( - - - {t("adminUsers.viewCard", "卡片")} - - ), - }, - { - value: "table", - label: ( - - - {t("adminUsers.viewTable", "表格")} - - ), - }, - ]} - /> - - -
-
- -
- - {showCardView ? ( - { - setResetTarget(row); - resetForm.resetFields(); - }} - onDelete={onDelete} - onUnlockLogin={onUnlockLogin} - nowSec={nowSec} - isSelfAdmin={isSelfAdmin} - /> - ) : ( - - rowKey="id" - loading={loading} - dataSource={filteredRows} - pagination={false} - scroll={{ x: "max-content" }} - columns={[ - { title: t("adminUsers.colId"), dataIndex: "id", width: 60 }, - { title: t("adminUsers.colUsername"), dataIndex: "username" }, - { - title: t("adminUsers.colDisplayName"), - dataIndex: "display_name", - }, - { - title: t("adminUsers.colCreatedAt"), - dataIndex: "created_at", - width: 168, - render: (ts: number | undefined) => ( - - {formatUserTs(ts, timeZone)} - - ), - }, - { - title: t("adminUsers.colAgents"), - width: 96, - render: (_, row) => { - const count = agentsByUserId.get(row.id)?.length ?? 0; - return ( - - ); - }, - }, - { - title: t("adminUsers.colRole"), - render: (_, row) => ( - - } autoFocus /> - - - } /> - - - } - autoComplete="new-password" - /> - - ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - return Promise.reject( - new Error(t("wizard.admin.passwordMismatch")), - ); - }, - }), - ]} - > - } - autoComplete="new-password" - /> - - - - - - - - { - setResetTarget(null); - resetForm.resetFields(); - }} - onOk={() => resetForm.submit()} - okText={t("common.reset")} - cancelText={t("common.cancel")} - confirmLoading={resetSubmitting} - > - - {t("adminUsers.resetHint")} - - - form={resetForm} - layout="vertical" - onFinish={onResetSubmit} - > - - - } - /> - - ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - return Promise.reject( - new Error(t("wizard.admin.passwordMismatch")), - ); - }, - }), - ]} - > - - } - /> - - - - + } + > + {activeTab === "local" ? : } + ); } diff --git a/dashboard/src/pages/Agent/Channels/ChannelsPanel.tsx b/dashboard/src/pages/Agent/Channels/ChannelsPanel.tsx index 165e6af5..9c0971f7 100644 --- a/dashboard/src/pages/Agent/Channels/ChannelsPanel.tsx +++ b/dashboard/src/pages/Agent/Channels/ChannelsPanel.tsx @@ -16,7 +16,7 @@ import { useChannels, CHANNEL_KEYS, DEFAULT_CHANNEL_DISPLAY_CONFIG, - CHANNEL_BOOLEAN_CONFIG_KEYS, + CHANNEL_DISPLAY_CONFIG_KEYS, CHANNEL_FIELDS, DEFAULT_QQ_GROUP_CONTEXT_CONFIG, normalizeChannelFieldValue, @@ -30,8 +30,11 @@ import styles from "./index.module.less"; function configFromFormValues( values: ChannelFormValues, ): Record { - const { __raw_config, show_thinking, show_tool_hints } = values; + const { __raw_config, response_mode, show_thinking, show_tool_hints } = + values; let config: Record = { + response_mode: + response_mode ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, show_thinking: show_thinking ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_thinking, show_tool_hints: @@ -46,6 +49,7 @@ function configFromFormValues( k === "name" || k === "enabled" || k === "__raw_config" || + k === "response_mode" || k === "show_thinking" || k === "show_tool_hints" ) { @@ -63,6 +67,15 @@ function configFromFormValues( } } } + config = { + ...config, + response_mode: + response_mode ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, + show_thinking: + show_thinking ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_thinking, + show_tool_hints: + show_tool_hints ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_tool_hints, + }; return config; } @@ -160,8 +173,8 @@ export default function ChannelsPanel({ agentId }: ChannelsPanelProps) { for (const [k, v] of Object.entries(cfg)) { if (v === undefined || v === null) continue; if ( - CHANNEL_BOOLEAN_CONFIG_KEYS.includes( - k as (typeof CHANNEL_BOOLEAN_CONFIG_KEYS)[number], + CHANNEL_DISPLAY_CONFIG_KEYS.includes( + k as (typeof CHANNEL_DISPLAY_CONFIG_KEYS)[number], ) ) { continue; @@ -186,6 +199,10 @@ export default function ChannelsPanel({ agentId }: ChannelsPanelProps) { const next: ChannelFormValues = { kind: row.kind as ChannelKey, enabled: row.enabled, + response_mode: + cfg.response_mode === "stream" + ? "stream" + : DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, show_thinking: typeof cfg.show_thinking === "boolean" ? cfg.show_thinking diff --git a/dashboard/src/pages/Agent/Channels/components/ChannelDrawer.tsx b/dashboard/src/pages/Agent/Channels/components/ChannelDrawer.tsx index 8c393a34..305e0d06 100644 --- a/dashboard/src/pages/Agent/Channels/components/ChannelDrawer.tsx +++ b/dashboard/src/pages/Agent/Channels/components/ChannelDrawer.tsx @@ -74,6 +74,7 @@ export interface ChannelFormValues { kind: ChannelKey; name?: string; enabled?: boolean; + response_mode?: "invoke" | "stream"; show_thinking?: boolean; show_tool_hints?: boolean; group_context?: QqGroupContextConfig; @@ -390,6 +391,9 @@ function QqGroupContextPolicyFields({ function DisplaySettingsFields() { const { t } = useTranslation(); + const responseMode = + Form.useWatch("response_mode") ?? + DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode; return (
@@ -403,13 +407,31 @@ function DisplaySettingsFields() { > + + + - + - +
); @@ -766,10 +788,16 @@ export function ChannelDrawer({ const getDisplayConfig = useCallback((): Pick< ChannelFormValues, - "show_thinking" | "show_tool_hints" + "response_mode" | "show_thinking" | "show_tool_hints" > => { - const values = form.getFieldsValue(["show_thinking", "show_tool_hints"]); + const values = form.getFieldsValue([ + "response_mode", + "show_thinking", + "show_tool_hints", + ]); return { + response_mode: + values.response_mode ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, show_thinking: values.show_thinking ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_thinking, show_tool_hints: @@ -969,9 +997,17 @@ export function ChannelDrawer({ }; const handleFinish = (values: ChannelFormValues) => { - const { kind, __raw_config, show_thinking, show_tool_hints, ...rest } = - values; + const { + kind, + __raw_config, + response_mode, + show_thinking, + show_tool_hints, + ...rest + } = values; let config: Record = { + response_mode: + response_mode ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, show_thinking: show_thinking ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_thinking, show_tool_hints: @@ -995,6 +1031,15 @@ export function ChannelDrawer({ } } } + config = { + ...config, + response_mode: + response_mode ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode, + show_thinking: + show_thinking ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_thinking, + show_tool_hints: + show_tool_hints ?? DEFAULT_CHANNEL_DISPLAY_CONFIG.show_tool_hints, + }; void (async () => { const ok = await onSubmit(kind, kind, config, values.enabled ?? false); if (ok) clearFormDraft(draftScope); diff --git a/dashboard/src/pages/Agent/Channels/components/constants.test.ts b/dashboard/src/pages/Agent/Channels/components/constants.test.ts index 8f5c93e7..7eb71ee4 100644 --- a/dashboard/src/pages/Agent/Channels/components/constants.test.ts +++ b/dashboard/src/pages/Agent/Channels/components/constants.test.ts @@ -1,10 +1,17 @@ import { describe, expect, it } from "vitest"; import { + DEFAULT_CHANNEL_DISPLAY_CONFIG, DEFAULT_QQ_GROUP_CONTEXT_CONFIG, normalizeQqGroupContextConfig, } from "./constants"; +describe("channel display defaults", () => { + it("uses invoke delivery for external IM channels", () => { + expect(DEFAULT_CHANNEL_DISPLAY_CONFIG.response_mode).toBe("invoke"); + }); +}); + describe("normalizeQqGroupContextConfig", () => { it("uses safe QQ group defaults for missing config", () => { expect(normalizeQqGroupContextConfig(undefined)).toEqual( diff --git a/dashboard/src/pages/Agent/Channels/components/constants.ts b/dashboard/src/pages/Agent/Channels/components/constants.ts index a23ae441..e40f4a4c 100644 --- a/dashboard/src/pages/Agent/Channels/components/constants.ts +++ b/dashboard/src/pages/Agent/Channels/components/constants.ts @@ -362,14 +362,16 @@ export const CHANNEL_FIELDS: Partial> = { // dashboard & agentchat: no required credentials. }; -/** Config keys stored as booleans — excluded from credential string mapping. */ -export const CHANNEL_BOOLEAN_CONFIG_KEYS = [ +/** Display-only config keys — excluded from credential field mapping. */ +export const CHANNEL_DISPLAY_CONFIG_KEYS = [ "show_thinking", "show_tool_hints", + "response_mode", ] as const; /** Default per-channel display settings (harness-gateway ChannelConfig). */ export const DEFAULT_CHANNEL_DISPLAY_CONFIG = { + response_mode: "invoke" as const, show_thinking: false, show_tool_hints: false, } as const; diff --git a/dashboard/src/pages/Agent/Channels/components/index.ts b/dashboard/src/pages/Agent/Channels/components/index.ts index e1f5c294..2bfc62bf 100644 --- a/dashboard/src/pages/Agent/Channels/components/index.ts +++ b/dashboard/src/pages/Agent/Channels/components/index.ts @@ -18,7 +18,7 @@ export { CHANNEL_URLS, CHANNEL_FIELDS, REQUIRED_CREDENTIALS, - CHANNEL_BOOLEAN_CONFIG_KEYS, + CHANNEL_DISPLAY_CONFIG_KEYS, DEFAULT_CHANNEL_DISPLAY_CONFIG, DEFAULT_QQ_GROUP_CONTEXT_CONFIG, normalizeChannelFieldValue, diff --git a/dashboard/src/pages/Agent/Connectors/index.tsx b/dashboard/src/pages/Agent/Connectors/index.tsx index 2b7bdd33..70e84e5a 100644 --- a/dashboard/src/pages/Agent/Connectors/index.tsx +++ b/dashboard/src/pages/Agent/Connectors/index.tsx @@ -16,7 +16,8 @@ import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import PageShell from "../../../layouts/PageShell"; -import { useUserRole } from "../../../hooks/useUserRole"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; +import { userCan } from "../../../utils/permissions"; import { apiErrorMessage } from "../../../utils/apiError"; import { clearFormDraft, @@ -239,8 +240,8 @@ function ConnectorConfigDrawer({ onSaved: () => void; }) { const { t } = useTranslation(); - const role = useUserRole(); - const isAdmin = role === "admin"; + const user = useCurrentUser(); + const canInstallCli = userCan(user, "connectors"); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const [probing, setProbing] = useState(false); @@ -922,7 +923,7 @@ function ConnectorConfigDrawer({
{entry && isHostCliConnector(entry.kind) && ( <> - {isAdmin && ( + {canInstallCli && ( )} - {!isAdmin && !cliInfo?.installed && ( + {!canInstallCli && !cliInfo?.installed && ( {t( "connectors.cliInstallAdminOnly", diff --git a/dashboard/src/pages/Agent/Personalization/index.tsx b/dashboard/src/pages/Agent/Personalization/index.tsx index cfbd4bd2..8a123ed6 100644 --- a/dashboard/src/pages/Agent/Personalization/index.tsx +++ b/dashboard/src/pages/Agent/Personalization/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Empty } from "antd"; import { Bot, Brain, Notebook, Sparkles, Waypoints } from "lucide-react"; @@ -6,6 +6,8 @@ import PageShell, { pageShellStyles } from "../../../layouts/PageShell"; import { useAgent } from "../../../context/AgentContext"; import { useIsMobile } from "../../../hooks/useIsMobile"; import { usePathTabs } from "../../../hooks/usePathTabs"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; +import { userCan } from "../../../utils/permissions"; import SkillsTabs from "../Skills/components/SkillsTabs"; import SubagentManager from "../../Experts/components/SubagentManager"; import MBTISelector from "./components/MBTISelector"; @@ -39,30 +41,39 @@ const TAB_ICONS = { export default function PersonalizationPage() { const { t } = useTranslation(); const isMobile = useIsMobile(); + const user = useCurrentUser(); const { activeAgentId, agents } = useAgent(); const activeAgent = agents.find((a) => a.agent_id === activeAgentId); + const isAllowed = useCallback( + (tab: PersonalizationTab) => + tab !== "channels" || userCan(user, "channels"), + [user], + ); const { activeTab, handleTabChange, isMounted } = usePathTabs({ basePath: "/personalization", tabs: PERSONALIZATION_TABS, storageKey: "octop:personalization:tab", defaultTab: "skills", + isAllowed, }); const pathTabs = useMemo( () => ({ value: activeTab, onChange: handleTabChange, - options: PERSONALIZATION_TABS.map((value) => { - const Icon = TAB_ICONS[value]; - return { - value, - label: t(`personalization.tabs.${value}`), - icon: , - }; - }), + options: PERSONALIZATION_TABS.filter((value) => isAllowed(value)).map( + (value) => { + const Icon = TAB_ICONS[value]; + return { + value, + label: t(`personalization.tabs.${value}`), + icon: , + }; + }, + ), }), - [activeTab, handleTabChange, t], + [activeTab, handleTabChange, isAllowed, t], ); const pageTitle = `${t("personalization.title")} / ${t( diff --git a/dashboard/src/pages/Agent/Skills/components/AgentPickerModal.tsx b/dashboard/src/pages/Agent/Skills/components/AgentPickerModal.tsx index 0803f982..0cb1c7e5 100644 --- a/dashboard/src/pages/Agent/Skills/components/AgentPickerModal.tsx +++ b/dashboard/src/pages/Agent/Skills/components/AgentPickerModal.tsx @@ -3,6 +3,7 @@ import { Modal, List, Avatar, Empty } from "antd"; import { useTranslation } from "react-i18next"; import { useAgent } from "../../../../context/AgentContext"; import type { OctopAgent } from "../../../../context/AgentContext"; +import { ownedExperts } from "../../../../utils/sharedExpert"; interface AgentPickerModalProps { open: boolean; @@ -17,6 +18,7 @@ export default function AgentPickerModal({ }: AgentPickerModalProps) { const { t } = useTranslation(); const { agents } = useAgent(); + const selectable = ownedExperts(agents); return ( - {agents.length === 0 ? ( + {selectable.length === 0 ? ( ) : ( ( Promise; @@ -46,6 +49,7 @@ interface InstalledSkillsTabProps { export default function InstalledSkillsTab({ kind, + agentId, skills, loading, fetchSkills, @@ -59,6 +63,14 @@ export default function InstalledSkillsTab({ deleteSkill, }: InstalledSkillsTabProps) { const { t } = useTranslation(); + const { agents } = useAgent(); + const workspaceReady = useMemo( + () => + isAgentChatReady( + agents.find((agent) => agent.agent_id === agentId)?.state, + ), + [agentId, agents], + ); const { viewMode, setViewMode, showCardView } = useCardTableView("card"); const [refreshing, setRefreshing] = useState(false); @@ -269,6 +281,8 @@ export default function InstalledSkillsTab({ open={drawerOpen} editingSkill={editingSkill} form={form} + agentId={agentId} + workspaceReady={workspaceReady} onClose={handleDrawerClose} onSubmit={handleSubmit} /> diff --git a/dashboard/src/pages/Agent/Skills/components/SkillDrawer.module.less b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.module.less index a1f8a9b9..d1ee26a3 100644 --- a/dashboard/src/pages/Agent/Skills/components/SkillDrawer.module.less +++ b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.module.less @@ -5,6 +5,82 @@ min-height: 0; } +.splitBody { + display: flex; + min-height: 0; + overflow: hidden; +} + +.singleBody { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.mainPane { + position: relative; + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mainPaneTreeCollapsed { + padding-left: 52px; +} + +.fileTreeExpandBtn { + position: absolute; + top: 16px; + left: 12px; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--fn-border-secondary); + border-radius: 8px; + background: var(--fn-bg-container, var(--fn-bg-primary)); + color: var(--fn-text-tertiary); + box-shadow: var(--fn-shadow-sm, 0 1px 4px rgb(0 0 0 / 8%)); + cursor: pointer; + transition: + background 0.15s ease, + color 0.15s ease, + border-color 0.15s ease; + + &:hover { + background: var(--fn-bg-hover, var(--fn-bg-secondary)); + color: var(--fn-text-primary); + border-color: var(--fn-border-primary); + } +} + +.siblingLoading { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +} + +.siblingEmpty { + min-height: 200px; + padding: 12px; + color: var(--fn-text-tertiary); + font-size: 13px; +} + +.siblingViewer { + flex: 1; + min-height: 200px; + overflow: auto; +} + .createForm { min-height: 0; overflow: hidden; diff --git a/dashboard/src/pages/Agent/Skills/components/SkillDrawer.test.ts b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.test.ts new file mode 100644 index 00000000..47610dec --- /dev/null +++ b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { + buildSkillMarkdown, + OCTOP_EMOJI_META_KEY, + parseSkillEmojiAndMetadata, +} from "./SkillDrawer"; + +describe("SkillDrawer emoji metadata", () => { + it("writes octop.emoji into frontmatter from the emoji field", () => { + const md = buildSkillMarkdown({ + name: "demo", + description: "A demo skill", + emoji: "⚙️", + metadata: [], + body: "Do things.", + }); + expect(md).toMatch(/emoji:\s*"?⚙️"?/); + expect(md).toContain("octop:"); + }); + + it("extracts emoji from flattened metadata and keeps other keys", () => { + const { emoji, metadata } = parseSkillEmojiAndMetadata([ + { key: OCTOP_EMOJI_META_KEY, value: "🔧" }, + { key: "octop.requires.bins", value: "git" }, + ]); + expect(emoji).toBe("🔧"); + expect(metadata).toEqual([{ key: "octop.requires.bins", value: "git" }]); + }); +}); diff --git a/dashboard/src/pages/Agent/Skills/components/SkillDrawer.tsx b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.tsx index a5c997dc..bd9aa1c3 100644 --- a/dashboard/src/pages/Agent/Skills/components/SkillDrawer.tsx +++ b/dashboard/src/pages/Agent/Skills/components/SkillDrawer.tsx @@ -1,13 +1,27 @@ -import { useEffect, useState } from "react"; -import { Drawer, Form, Input, Button, Segmented } from "antd"; +import { useCallback, useEffect, useState } from "react"; +import { Drawer, Form, Input, Button, Segmented, Tooltip } from "antd"; import { message } from "@/utils/antdMessage"; -import { MinusCircle, Plus } from "lucide-react"; +import { MinusCircle, PanelLeftOpen, Plus } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { FormInstance } from "antd"; +import EmojiPicker from "../../../../components/EmojiPicker"; import Markdown from "../../../../components/Markdown/LazyMarkdown"; +import { request } from "../../../../api/request"; import { splitMarkdownFrontmatter } from "../../../../utils/markdown"; +import { withFromWorkspace } from "../../../../utils/fromWorkspace"; +import { useListPanelCollapsed } from "../../../../hooks/useListPanelCollapsed"; import type { SkillDetail } from "../useSkills"; +import { + isSkillManifestPath, + skillDirectoryPath, + skillManifestPath, + DEFAULT_SKILL_EMOJI, +} from "../skillMarkdown"; +import FileViewer from "../../Workspace/components/FileViewer"; +import { getDocKind } from "../../Workspace/utils/docKind"; +import { getMediaKind } from "../../Workspace/utils/mediaKind"; +import { SkillFileTree } from "./SkillFileTree"; import styles from "./SkillDrawer.module.less"; export interface MetadataEntry { @@ -19,6 +33,8 @@ export interface MetadataEntry { export interface SkillFormValues { name: string; description: string; + /** Surfaced as ``metadata.octop.emoji`` in SKILL.md. */ + emoji: string; metadata: MetadataEntry[]; body: string; content?: string; @@ -26,6 +42,8 @@ export interface SkillFormValues { path?: string; } +export const OCTOP_EMOJI_META_KEY = "octop.emoji"; + function yamlQuote(value: string): string { if (!value) return '""'; if (/[:#\n"'{}[\],&*?|>!%@`]/.test(value) || value.trim() !== value) { @@ -72,6 +90,36 @@ function metadataToYamlLines( return lines; } +/** Split ``octop.emoji`` out of flattened metadata for the dedicated picker. */ +export function parseSkillEmojiAndMetadata( + pairs: MetadataEntry[] | undefined, + fallback = DEFAULT_SKILL_EMOJI, +): { emoji: string; metadata: MetadataEntry[] } { + let emoji = fallback; + const metadata: MetadataEntry[] = []; + for (const row of pairs ?? []) { + if (row.key.trim() === OCTOP_EMOJI_META_KEY) { + const value = row.value.trim(); + if (value) emoji = value; + continue; + } + metadata.push(row); + } + return { emoji, metadata }; +} + +function withEmojiMetadata( + pairs: MetadataEntry[] | undefined, + emoji: string, +): MetadataEntry[] { + const rest = (pairs ?? []).filter( + (row) => row.key.trim() !== OCTOP_EMOJI_META_KEY, + ); + const value = emoji.trim(); + if (!value) return rest; + return [{ key: OCTOP_EMOJI_META_KEY, value }, ...rest]; +} + function buildMetadataObject( pairs: MetadataEntry[] | undefined, ): Record { @@ -90,7 +138,9 @@ export function buildSkillMarkdown(values: SkillFormValues): string { `name: ${yamlQuote(values.name.trim())}`, `description: ${yamlQuote(values.description.trim())}`, ]; - const meta = buildMetadataObject(values.metadata); + const meta = buildMetadataObject( + withEmojiMetadata(values.metadata, values.emoji ?? ""), + ); if (Object.keys(meta).length > 0) { lines.push("metadata:"); lines.push(...metadataToYamlLines(meta, 1)); @@ -145,10 +195,14 @@ function parseSkillFormFromDetail(detail: SkillDetail): SkillFormValues { typeof fm.name === "string" && fm.name.trim() ? fm.name : detail.slug; const description = typeof fm.description === "string" ? fm.description : detail.description; - const metadata = flattenMetadata(fm.metadata); + const { emoji, metadata } = parseSkillEmojiAndMetadata( + flattenMetadata(fm.metadata), + detail.emoji?.trim() || DEFAULT_SKILL_EMOJI, + ); return { name: displayName, description, + emoji, metadata, body: detail.body || "", content: detail.raw, @@ -163,12 +217,18 @@ function parseSkillFormFromDetail(detail: SkillDetail): SkillFormValues { type ViewTab = "preview" | "source"; type EditorTab = "form" | "source"; +const FILE_TREE_COLLAPSED_KEY = "octop:skill-drawer-tree-collapsed"; + interface SkillDrawerProps { open: boolean; editingSkill: SkillDetail | null; form: FormInstance; onClose: () => void; onSubmit: (values: SkillFormValues) => void; + /** When set, show skill directory file tree (workspace / built-in skills). */ + agentId?: string | null; + /** Agent harness must be running for workspace file/tree APIs. */ + workspaceReady?: boolean; } export function SkillDrawer({ @@ -177,14 +237,39 @@ export function SkillDrawer({ form, onClose, onSubmit, + agentId, + workspaceReady = false, }: SkillDrawerProps) { const { t } = useTranslation(); const isCreate = !editingSkill; const [localEditMode, setLocalEditMode] = useState(false); const [viewTab, setViewTab] = useState("preview"); const [editorTab, setEditorTab] = useState("form"); - + const [selectedFilePath, setSelectedFilePath] = useState(null); + const [siblingContent, setSiblingContent] = useState(""); + const [siblingLoading, setSiblingLoading] = useState(false); + const [siblingViewTab, setSiblingViewTab] = useState("preview"); + const { collapsed: fileTreeCollapsed, toggle: toggleFileTreeCollapsed } = + useListPanelCollapsed(FILE_TREE_COLLAPSED_KEY, { defaultCollapsed: true }); + + const skillRoot = editingSkill ? skillDirectoryPath(editingSkill) : null; + const showFileTree = Boolean(editingSkill && agentId && skillRoot); const isEdit = !!editingSkill && localEditMode; + + const handleSelectFilePath = useCallback( + (path: string) => { + if (isEdit && !isSkillManifestPath(path)) { + message.warning(t("skills.finishEditBeforeSwitchFile")); + return; + } + setSelectedFilePath(path); + }, + [isEdit, t], + ); + + const viewingSkillMd = + !selectedFilePath || isSkillManifestPath(selectedFilePath); + const fieldsEditable = isCreate || isEdit; useEffect(() => { @@ -192,12 +277,17 @@ export function SkillDrawer({ setLocalEditMode(false); setViewTab("preview"); setEditorTab("form"); + setSelectedFilePath(null); + setSiblingContent(""); + setSiblingViewTab("preview"); return; } setLocalEditMode(false); setViewTab("preview"); setEditorTab("form"); + setSiblingViewTab("preview"); if (editingSkill) { + setSelectedFilePath(skillManifestPath(editingSkill)); const parsed = parseSkillFormFromDetail(editingSkill); form.setFieldsValue({ ...parsed, @@ -208,15 +298,64 @@ export function SkillDrawer({ }); return; } + setSelectedFilePath(null); form.setFieldsValue({ name: "", description: "", - metadata: [{ key: "octop.emoji", value: "✨" }], + emoji: DEFAULT_SKILL_EMOJI, + metadata: [], body: t("skills.newSkillBodyTemplate"), content: "", }); }, [editingSkill, form, open, t]); + const loadSiblingFile = useCallback( + async (path: string) => { + if (!agentId || isSkillManifestPath(path)) return; + setSiblingLoading(true); + try { + const data = await request<{ content: string }>( + withFromWorkspace( + `/agents/${agentId}/workspace/file?path=${encodeURIComponent( + path, + )}`, + ), + ); + setSiblingContent(data.content ?? ""); + } catch { + setSiblingContent(""); + } finally { + setSiblingLoading(false); + } + }, + [agentId], + ); + + useEffect(() => { + if ( + !open || + !selectedFilePath || + viewingSkillMd || + !agentId || + !workspaceReady + ) { + return; + } + if (getMediaKind(selectedFilePath) || getDocKind(selectedFilePath)) { + setSiblingContent(""); + setSiblingLoading(false); + return; + } + void loadSiblingFile(selectedFilePath); + }, [ + agentId, + loadSiblingFile, + open, + selectedFilePath, + viewingSkillMd, + workspaceReady, + ]); + const resetToViewForm = () => { if (!editingSkill) return; const parsed = parseSkillFormFromDetail(editingSkill); @@ -324,6 +463,20 @@ export function SkillDrawer({ ); + const emojiField = ( + + {fieldsEditable ? ( + + ) : ( + + )} + + ); + const metadataFields = (
@@ -471,9 +624,104 @@ export function SkillDrawer({
); + const siblingFileName = + selectedFilePath?.split("/").filter(Boolean).pop() ?? ""; + const siblingUsesRichPreview = Boolean( + selectedFilePath && + (getMediaKind(selectedFilePath) || getDocKind(selectedFilePath)), + ); + + const siblingFileBlock = ( +
+
+ {siblingFileName} + {!siblingUsesRichPreview ? ( + setSiblingViewTab(value as ViewTab)} + options={[ + { value: "preview", label: t("skills.viewPreview") }, + { value: "source", label: t("skills.viewSource") }, + ]} + /> + ) : null} +
+ {!workspaceReady ? ( +
+ {t("skills.fileTreeAgentNotReady")} +
+ ) : agentId && selectedFilePath ? ( +
+ {}} + fileLoading={siblingLoading} + previewMode={siblingViewTab === "preview"} + /> +
+ ) : ( +
+ )} +
+ ); + + const mainPanel = ( +
+ {viewingSkillMd ? ( + isCreate || isEdit ? ( +
+ {editorModeToggle} + {editorTab === "form" ? ( + <> +
+ {nameField} + {descriptionField} + {emojiField} + {metadataFields} +
+ {bodyEditBlock} + + ) : ( + sourceEditBlock + )} +
+ ) : ( +
+ {nameField} + {descriptionField} + {emojiField} + + + + + + + {viewContentBlock} +
+ ) + ) : ( +
{siblingFileBlock}
+ )} +
+ ); + return (
-
- {isCreate || isEdit ? ( -
- {editorModeToggle} - {editorTab === "form" ? ( - <> -
- {nameField} - {descriptionField} - {metadataFields} -
- {bodyEditBlock} - - ) : ( - sourceEditBlock - )} -
- ) : ( -
- {nameField} - {descriptionField} - - - - - - - {viewContentBlock} -
- )} -
+
+ {showFileTree && skillRoot && agentId && !fileTreeCollapsed ? ( + + ) : null} +
+ {showFileTree && fileTreeCollapsed ? ( + + + + ) : null} + {mainPanel} +
+
{isCreate ? ( @@ -545,10 +791,13 @@ export function SkillDrawer({ ) : ( <> - {editingSkill?.kind === "workspace" ? ( + {editingSkill?.kind === "workspace" && viewingSkillMd ? ( + +
+
+ {!workspaceReady ? ( +
+ {t("skills.fileTreeAgentNotReady")} +
+ ) : loading ? ( +
+ +
+ ) : treeData.length === 0 ? ( +
{t("skills.fileTreeEmpty")}
+ ) : ( + setExpandedKeys(keys as string[])} + selectedKeys={selectedKey ? [selectedKey] : []} + onSelect={(_keys, info) => { + if (selectionDisabled) { + message.warning(t("skills.finishEditBeforeSwitchFile")); + return; + } + const { path, is_dir } = pathFromKey(String(info.node.key)); + if (is_dir) return; + onSelectPath(path); + }} + className={styles.tree} + /> + )} +
+
+ ); +} diff --git a/dashboard/src/pages/Agent/Skills/components/SkillImportModal.test.tsx b/dashboard/src/pages/Agent/Skills/components/SkillImportModal.test.tsx index 43faa0bd..cf8f6beb 100644 --- a/dashboard/src/pages/Agent/Skills/components/SkillImportModal.test.tsx +++ b/dashboard/src/pages/Agent/Skills/components/SkillImportModal.test.tsx @@ -186,7 +186,9 @@ describe(" local zip import", () => { 'input[type="file"]', ) as HTMLInputElement; setInputFiles(fileInput, [zipFile]); - await user.click(screen.getByText("skills.removeZip")); + const removeButtons = screen.getAllByText("skills.removeZip"); + // Click either remove button + await user.click(removeButtons[0]); expect( screen.getByRole("button", { name: "skills.importSkills" }), ).toBeDisabled(); diff --git a/dashboard/src/pages/Agent/Skills/components/SkillImportModal.tsx b/dashboard/src/pages/Agent/Skills/components/SkillImportModal.tsx index 28fb2265..c19a253d 100644 --- a/dashboard/src/pages/Agent/Skills/components/SkillImportModal.tsx +++ b/dashboard/src/pages/Agent/Skills/components/SkillImportModal.tsx @@ -59,6 +59,7 @@ export function SkillImportModal({ const [zipError, setZipError] = useState(""); const [overwrite, setOverwrite] = useState(false); const [parsing, setParsing] = useState(false); + const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); useEffect(() => { @@ -70,9 +71,24 @@ export function SkillImportModal({ setZipError(""); setOverwrite(false); setParsing(false); + setIsDragging(false); } }, [open]); + const handleFileSelect = (file: File | null) => { + if (!file) { + setZipFile(null); + return; + } + if (!file.name.toLowerCase().endsWith(".zip")) { + setZipFile(null); + setZipError(t("skills.zipOnly")); + return; + } + setZipError(""); + setZipFile(file); + }; + const handleUrlChange = (value: string) => { setImportUrl(value); const trimmed = value.trim(); @@ -133,6 +149,28 @@ export function SkillImportModal({ ? !importUrl.trim() || !!importUrlError : !zipFile || !!zipError); + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + if (busy) return; + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (busy) return; + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + handleFileSelect(files[0]); + } + }; + return (
+
!busy && fileInputRef.current?.click()} + > +
+ + + + + +
+
+ {zipFile ? t("skills.zipSelected", { name: zipFile.name }) : t("skills.zipDragDropHint")} +
+ {zipFile ? ( + + ) : null} +
+ { const next = event.target.files?.[0] ?? null; event.target.value = ""; - setZipError(""); - if (!next) { - setZipFile(null); - return; - } - if (!next.name.toLowerCase().endsWith(".zip")) { - setZipFile(null); - setZipError(t("skills.zipOnly")); - return; - } - setZipFile(next); + handleFileSelect(next); }} /> +
) : null}
+ {zipError ? (
{zipError}
) : null} diff --git a/dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx b/dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx index f8511ac7..128803c2 100644 --- a/dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx +++ b/dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx @@ -83,6 +83,7 @@ export default function SkillsTabs({ agentId }: SkillsTabsProps) { agentId ? ( diff --git a/dashboard/src/pages/Agent/Skills/index.module.less b/dashboard/src/pages/Agent/Skills/index.module.less index c735c6d5..b660d39b 100644 --- a/dashboard/src/pages/Agent/Skills/index.module.less +++ b/dashboard/src/pages/Agent/Skills/index.module.less @@ -965,6 +965,7 @@ align-items: center; gap: 12px; flex-wrap: wrap; + margin-top: 16px; } .zipFileName { @@ -972,3 +973,47 @@ font-size: 13px; word-break: break-all; } + +.zipDragDrop { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 32px 24px; + border: 2px dashed var(--fn-border-input); + border-radius: var(--fn-radius-md); + cursor: pointer; + transition: all 0.2s ease; + background-color: var(--fn-bg-tertiary); + + &:hover { + border-color: var(--fn-color-brand); + background-color: color-mix(in srgb, var(--fn-color-brand) 5%, var(--fn-bg-tertiary)); + } +} + +.zipDragDropActive { + border-color: var(--fn-color-brand); + background-color: color-mix(in srgb, var(--fn-color-brand) 10%, var(--fn-bg-tertiary)); + transform: scale(1.01); +} + +.zipDragDropHasFile { + border-style: solid; + border-color: var(--fn-color-brand); +} + +.zipDragDropIcon { + color: var(--fn-text-tertiary); + opacity: 0.8; + display: flex; + align-items: center; + justify-content: center; +} + +.zipDragDropText { + color: var(--fn-text-secondary); + font-size: 14px; + text-align: center; +} diff --git a/dashboard/src/pages/Agent/Skills/skillMarkdown.test.ts b/dashboard/src/pages/Agent/Skills/skillMarkdown.test.ts new file mode 100644 index 00000000..0090a904 --- /dev/null +++ b/dashboard/src/pages/Agent/Skills/skillMarkdown.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { buildSkillMarkdown } from "./components/SkillDrawer"; +import { + isSkillManifestPath, + parseSkillPreviewFromMarkdown, + skillDirectoryPath, + skillManifestPath, +} from "./skillMarkdown"; + +describe("parseSkillPreviewFromMarkdown", () => { + it("reads name, description, and octop emoji from SKILL.md", () => { + const md = buildSkillMarkdown({ + name: "My Skill", + description: "Does work", + emoji: "🛠️", + metadata: [], + body: "Body", + }); + expect(parseSkillPreviewFromMarkdown(md, "my-skill")).toEqual({ + emoji: "🛠️", + name: "My Skill", + description: "Does work", + }); + }); +}); + +describe("isSkillManifestPath", () => { + it("matches SKILL.md paths", () => { + expect(isSkillManifestPath("/skills/foo/SKILL.md")).toBe(true); + expect(isSkillManifestPath("notes.txt")).toBe(false); + }); +}); + +describe("skillDirectoryPath", () => { + it("maps workspace and builtin roots", () => { + expect(skillDirectoryPath({ kind: "workspace", slug: "demo" })).toBe( + "/skills/demo", + ); + expect(skillDirectoryPath({ kind: "builtin", slug: "demo" })).toBe( + "/_builtin_skills/demo", + ); + }); + + it("builds manifest path", () => { + expect(skillManifestPath({ kind: "workspace", slug: "demo" })).toBe( + "/skills/demo/SKILL.md", + ); + }); +}); diff --git a/dashboard/src/pages/Agent/Skills/skillMarkdown.ts b/dashboard/src/pages/Agent/Skills/skillMarkdown.ts new file mode 100644 index 00000000..c17b38ff --- /dev/null +++ b/dashboard/src/pages/Agent/Skills/skillMarkdown.ts @@ -0,0 +1,53 @@ +import { splitMarkdownFrontmatter } from "../../../utils/markdown"; +import type { SkillDetail } from "./useSkills"; + +export const DEFAULT_SKILL_EMOJI = "✨"; + +function yamlTopLevel(block: string, key: string): string { + const re = new RegExp(`^${key}:\\s*(.+?)\\s*$`, "m"); + const match = block.match(re); + if (!match) return ""; + return (match[1] || "").trim().replace(/^["']|["']$/g, ""); +} + +/** Parse display fields from raw SKILL.md for list previews (e.g. expert template). */ +export function parseSkillPreviewFromMarkdown( + content: string, + slug: string, +): { emoji: string; name: string; description: string } { + const { raw } = splitMarkdownFrontmatter(content); + const fm = raw ?? ""; + const name = yamlTopLevel(fm, "name") || slug; + const description = yamlTopLevel(fm, "description") || ""; + let emoji = DEFAULT_SKILL_EMOJI; + const emojiMatch = fm.match(/octop:\s*\n\s*emoji:\s*(.+)/); + if (emojiMatch?.[1]) { + const value = emojiMatch[1].trim().replace(/^["']|["']$/g, ""); + if (value) emoji = value; + } + return { emoji, name, description }; +} + +export function skillDirectoryPath( + detail: Pick, +): string { + return detail.kind === "builtin" + ? `/_builtin_skills/${detail.slug}` + : `/skills/${detail.slug}`; +} + +export function isSkillManifestPath(path: string): boolean { + const normalized = path.replace(/\\/g, "/"); + return ( + normalized.endsWith("/SKILL.md") || + normalized === "SKILL.md" || + normalized.endsWith("/skill.md") || + normalized === "skill.md" + ); +} + +export function skillManifestPath( + detail: Pick, +): string { + return `${skillDirectoryPath(detail)}/SKILL.md`; +} diff --git a/dashboard/src/pages/Chat/chatAgentCard.partial.less b/dashboard/src/pages/Chat/chatAgentCard.partial.less index 94903df1..141f6678 100644 --- a/dashboard/src/pages/Chat/chatAgentCard.partial.less +++ b/dashboard/src/pages/Chat/chatAgentCard.partial.less @@ -44,6 +44,21 @@ font-weight: 500; color: var(--fn-text-primary); line-height: 1.35; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sharedExpertHint { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 16px; + height: 16px; + color: var(--fn-text-tertiary); + border-radius: 999px; } .agentCardDesc { diff --git a/dashboard/src/pages/Chat/chatContextChips.partial.less b/dashboard/src/pages/Chat/chatContextChips.partial.less index 9fd4f44e..b9d9c817 100644 --- a/dashboard/src/pages/Chat/chatContextChips.partial.less +++ b/dashboard/src/pages/Chat/chatContextChips.partial.less @@ -139,6 +139,30 @@ } } +.contextChipKnowledge { + border-color: rgba(14, 165, 233, 0.24); + background: rgba(14, 165, 233, 0.08); + + .contextChipIcon { + border-radius: 50%; + background: rgba(14, 165, 233, 0.14); + color: #0284c7; + } + + .contextChipLabel { + color: #0369a1; + } + + .contextChipRemove { + color: rgba(3, 105, 161, 0.55); + + &:hover { + color: #0369a1; + background: rgba(14, 165, 233, 0.12); + } + } +} + .contextChipExpert { border-color: rgba(99, 102, 241, 0.22); background: rgba(99, 102, 241, 0.08); diff --git a/dashboard/src/pages/Chat/chatInputCore.partial.less b/dashboard/src/pages/Chat/chatInputCore.partial.less index 5bba4031..d25ae392 100644 --- a/dashboard/src/pages/Chat/chatInputCore.partial.less +++ b/dashboard/src/pages/Chat/chatInputCore.partial.less @@ -645,6 +645,12 @@ border-radius: var(--fn-radius-sm); overflow: hidden; border: 1px solid var(--fn-border-secondary); + line-height: 0; + + :global(.ant-image) { + display: block; + line-height: 0; + } } .attachmentPreviewCard { @@ -682,8 +688,8 @@ } .imagePreviewThumb { - width: 100%; - height: 100%; + width: 64px; + height: 64px; object-fit: cover; display: block; } diff --git a/dashboard/src/pages/Chat/chatInputPickers.partial.less b/dashboard/src/pages/Chat/chatInputPickers.partial.less index a6b7e26c..178e719e 100644 --- a/dashboard/src/pages/Chat/chatInputPickers.partial.less +++ b/dashboard/src/pages/Chat/chatInputPickers.partial.less @@ -422,6 +422,19 @@ background: var(--fn-bg-subtle, #f3f4f6); } +.knowledgePickerAvatar { + flex-shrink: 0; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + overflow: hidden; + color: #0284c7; + background: rgba(14, 165, 233, 0.12); +} + .shortcutPickerIcon { flex-shrink: 0; width: 36px; diff --git a/dashboard/src/pages/Chat/chatMessages.partial.less b/dashboard/src/pages/Chat/chatMessages.partial.less index 51ab5f46..9631c0c7 100644 --- a/dashboard/src/pages/Chat/chatMessages.partial.less +++ b/dashboard/src/pages/Chat/chatMessages.partial.less @@ -1027,6 +1027,12 @@ color: var(--fn-text-primary); background: var(--fn-bg-hover); } + + .msgActionBtn:disabled, + .msgActionBtn:disabled:hover { + cursor: not-allowed; + opacity: 0.35; + } } /* Reveal when the user bubble is hovered/focused, not only the tiny buttons. */ diff --git a/dashboard/src/pages/Chat/components/ChatInput.tsx b/dashboard/src/pages/Chat/components/ChatInput.tsx index 46e774b2..898dfa8b 100644 --- a/dashboard/src/pages/Chat/components/ChatInput.tsx +++ b/dashboard/src/pages/Chat/components/ChatInput.tsx @@ -16,6 +16,7 @@ import SlashCommandMenu from "./SlashCommandMenu"; import { agentChatApi } from "../../../api/modules/agentChat"; import type { ChatAttachment } from "../hooks/useChat"; import type { ResolvedModel } from "../../../api/types"; +import type { KnowledgeBase } from "../../../api/modules/knowledgeBases"; import type { SkillSpec } from "../../Agent/Skills/useSkills"; import type { ChatAgentOption } from "./ExpertAgentAvatar"; import MentionPickerMenu from "./MentionPickerMenu"; @@ -27,7 +28,11 @@ import { useKeyboardOffset } from "../../../hooks/useKeyboardOffset"; import { useChatAttachments } from "../hooks/useChatAttachments"; import { useSlashMentionInput } from "../hooks/useSlashMentionInput"; import { stripThinkingTags } from "../utils/chatAttachments"; -import { readInputDraft, writeInputDraft } from "../hooks/chatStore"; +import { + consumePendingPrefillAttachments, + readInputDraft, + writeInputDraft, +} from "../hooks/chatStore"; import { buildComposerContext, resolveTurnModelRef, @@ -41,6 +46,7 @@ import styles from "../index.module.less"; /** Imperative handle exposed via ref for programmatic text injection. */ export interface ChatInputHandle { setPrefillText: (text: string) => void; + setPrefillComposer: (text: string, attachments?: ChatAttachment[]) => void; } interface ChatInputProps { @@ -81,6 +87,9 @@ interface ChatInputProps { }[]; selectedConnectors?: string[]; onConnectorsChange?: (names: string[]) => void; + availableKnowledgeBases?: KnowledgeBase[]; + selectedKnowledgeBaseIds?: string[]; + onKnowledgeBaseIdsChange?: (ids: string[]) => void; availableSkills?: SkillSpec[]; selectedSkills?: string[]; onSkillsChange?: (names: string[]) => void; @@ -124,6 +133,9 @@ const ChatInput = forwardRef( availableConnectors, selectedConnectors = [], onConnectorsChange, + availableKnowledgeBases, + selectedKnowledgeBaseIds = [], + onKnowledgeBaseIdsChange, availableSkills, selectedSkills = [], onSkillsChange, @@ -167,24 +179,70 @@ const ChatInput = forwardRef( transcribing, toggle: toggleVoice, } = useVoiceInput(handleVoiceText); + const textareaRef = useRef(null); + const { + attachments, + uploading, + dragOver, + fileInputRef, + acceptAttr, + handleFileSelect, + handleFileChange, + removeAttachment, + clearAttachments, + restoreAttachments, + handlePaste, + handleDragEnter, + handleDragLeave, + handleDragOver, + handleDrop, + } = useChatAttachments(agentId); // Expose an imperative handle so the parent can push a new prefill without // triggering a prop change that would cause a re-render cascade. - useImperativeHandle(ref, () => ({ - setPrefillText: (newText: string) => { - userHasEditedRef.current = false; - ignoreInitialTextRef.current = null; - prevInitialTextRef.current = newText; - setText(newText); - setTimeout(() => { - const el = textareaRef.current; - if (el) { - el.focus(); - el.setSelectionRange(el.value.length, el.value.length); + useImperativeHandle( + ref, + () => ({ + setPrefillText: (newText: string) => { + userHasEditedRef.current = false; + ignoreInitialTextRef.current = null; + prevInitialTextRef.current = newText; + setText(newText); + setTimeout(() => { + const el = textareaRef.current; + if (el) { + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); + } + }, 50); + }, + setPrefillComposer: ( + newText: string, + nextAttachments?: ChatAttachment[], + ) => { + userHasEditedRef.current = false; + ignoreInitialTextRef.current = null; + prevInitialTextRef.current = newText; + setText(newText); + if (nextAttachments && nextAttachments.length > 0) { + restoreAttachments( + nextAttachments.map((attachment) => ({ ...attachment })), + ); + } else { + clearAttachments(); } - }, 50); - }, - })); + setTimeout(() => { + const el = textareaRef.current; + if (el) { + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); + } + }, 50); + }, + }), + [clearAttachments, restoreAttachments], + ); + // When the parent passes a non-empty initialText after mount (e.g. navigated // from cron-jobs), update the input value and move the cursor to the end. // Only fires when initialText actually changes AND the user hasn't started @@ -232,7 +290,13 @@ const ChatInput = forwardRef( ignoreInitialTextRef.current = null; prevInitialTextRef.current = ""; setText(initialText || readInputDraft(agentId, threadId)); - }, [agentId, threadId, initialText]); + const pendingAttachments = consumePendingPrefillAttachments(); + if (pendingAttachments.length > 0) { + restoreAttachments(pendingAttachments); + } else { + clearAttachments(); + } + }, [agentId, threadId, initialText, clearAttachments, restoreAttachments]); // Persist draft while typing so leaving /chat and returning keeps content. useEffect(() => { @@ -242,24 +306,6 @@ const ChatInput = forwardRef( }, 250); return () => window.clearTimeout(timer); }, [text, agentId, threadId]); - const { - attachments, - uploading, - dragOver, - fileInputRef, - acceptAttr, - handleFileSelect, - handleFileChange, - removeAttachment, - clearAttachments, - restoreAttachments, - handlePaste, - handleDragEnter, - handleDragLeave, - handleDragOver, - handleDrop, - } = useChatAttachments(agentId); - const textareaRef = useRef(null); const submitRef = useRef<() => void>(() => {}); const MIN_TEXTAREA_HEIGHT = isMobile ? 42 : 78; @@ -363,6 +409,7 @@ const ChatInput = forwardRef( composerContext: buildComposerContext({ skills: selectedSkills, connectors: selectedConnectors, + knowledgeBaseIds: selectedKnowledgeBaseIds, targetAgents: selectedTargetAgents, selectedModel, reasoningMode, @@ -416,6 +463,7 @@ const ChatInput = forwardRef( if (ctx) { onSkillsChange?.(ctx.skills ?? []); onConnectorsChange?.(ctx.connectors ?? []); + onKnowledgeBaseIdsChange?.(ctx.knowledgeBaseIds ?? []); onTargetAgentsChange?.(ctx.targetAgents ?? []); if (ctx.model !== undefined) { onModelChange?.(ctx.model); @@ -460,6 +508,7 @@ const ChatInput = forwardRef( t, onSkillsChange, onConnectorsChange, + onKnowledgeBaseIdsChange, onTargetAgentsChange, onModelChange, onReasoningChange, @@ -564,10 +613,13 @@ const ChatInput = forwardRef( selectedModel={selectedModel} availableSkills={availableSkills} availableConnectors={availableConnectors} + availableKnowledgeBases={availableKnowledgeBases} availableAgents={availableAgents} onRemoveAttachment={removeAttachment} onSkillsChange={onSkillsChange} onConnectorsChange={onConnectorsChange} + selectedKnowledgeBaseIds={selectedKnowledgeBaseIds} + onKnowledgeBaseIdsChange={onKnowledgeBaseIdsChange} onTargetAgentsChange={onTargetAgentsChange} onModelChange={onModelChange} /> @@ -696,6 +748,9 @@ const ChatInput = forwardRef( availableConnectors={availableConnectors} selectedConnectors={selectedConnectors} onConnectorsChange={onConnectorsChange} + availableKnowledgeBases={availableKnowledgeBases} + selectedKnowledgeBaseIds={selectedKnowledgeBaseIds} + onKnowledgeBaseIdsChange={onKnowledgeBaseIdsChange} availableSkills={availableSkills} selectedSkills={selectedSkills} onSkillsChange={onSkillsChange} diff --git a/dashboard/src/pages/Chat/components/ChatInputActionsRow.tsx b/dashboard/src/pages/Chat/components/ChatInputActionsRow.tsx index 983c1428..f74ab74e 100644 --- a/dashboard/src/pages/Chat/components/ChatInputActionsRow.tsx +++ b/dashboard/src/pages/Chat/components/ChatInputActionsRow.tsx @@ -16,6 +16,7 @@ import { Cpu, Brain, GraduationCap, + BookOpen, MoreHorizontal, Check, ChevronLeft, @@ -23,6 +24,7 @@ import { } from "lucide-react"; import { Tooltip, Popover, Drawer } from "antd"; import type { ResolvedModel } from "../../../api/types"; +import type { KnowledgeBase } from "../../../api/modules/knowledgeBases"; import type { SkillSpec } from "../../Agent/Skills/useSkills"; import type { ChatAgentOption } from "./ExpertAgentAvatar"; import { @@ -34,6 +36,7 @@ import ContextWindowRing from "./ContextWindowRing"; import SkillPickerPopover from "./SkillPickerPopover"; import ExpertPickerPopover from "./ExpertPickerPopover"; import ConnectorPickerPopover from "./ConnectorPickerPopover"; +import KnowledgePickerPopover from "./KnowledgePickerPopover"; import SlashCommandMenu from "./SlashCommandMenu"; import type { SlashMenuGroup } from "../../../utils/slashCategories"; import type { SlashMenuItem } from "../hooks/useSlashMentionInput"; @@ -42,7 +45,13 @@ import { isSttAvailable } from "../../../hooks/useVoiceInput"; import { resolveTurnModelOverride } from "../utils/chatMessages"; import styles from "../index.module.less"; -type MobilePickerKey = "model" | "connector" | "skill" | "expert" | "shortcut"; +type MobilePickerKey = + | "model" + | "connector" + | "knowledge" + | "skill" + | "expert" + | "shortcut"; // These browser APIs never change at runtime — compute once. const _sttAvailable = isSttAvailable(); @@ -84,6 +93,9 @@ interface ChatInputActionsRowProps { }[]; selectedConnectors?: string[]; onConnectorsChange?: (names: string[]) => void; + availableKnowledgeBases?: KnowledgeBase[]; + selectedKnowledgeBaseIds?: string[]; + onKnowledgeBaseIdsChange?: (ids: string[]) => void; availableSkills?: SkillSpec[]; selectedSkills?: string[]; onSkillsChange?: (names: string[]) => void; @@ -131,6 +143,9 @@ export default function ChatInputActionsRow({ availableConnectors, selectedConnectors = [], onConnectorsChange, + availableKnowledgeBases, + selectedKnowledgeBaseIds = [], + onKnowledgeBaseIdsChange, availableSkills, selectedSkills = [], onSkillsChange, @@ -153,6 +168,7 @@ export default function ChatInputActionsRow({ const [skillPickerOpen, setSkillPickerOpen] = useState(false); const [expertPickerOpen, setExpertPickerOpen] = useState(false); const [connectorPickerOpen, setConnectorPickerOpen] = useState(false); + const [knowledgePickerOpen, setKnowledgePickerOpen] = useState(false); const [shortcutOpen, setShortcutOpen] = useState(false); const [modelPickerOpen, setModelPickerOpen] = useState(false); const [reasoningModelRef, setReasoningModelRef] = useState( @@ -194,6 +210,9 @@ export default function ChatInputActionsRow({ const showConnectorPicker = Boolean( availableConnectors && onConnectorsChange, ); + const showKnowledgePicker = Boolean( + availableKnowledgeBases && onKnowledgeBaseIdsChange, + ); const showSkillPicker = Boolean(availableSkills && onSkillsChange); const showExpertPicker = Boolean( availableExperts && onTargetAgentsChange && availableExperts.length > 0, @@ -201,12 +220,14 @@ export default function ChatInputActionsRow({ const showShortcutPicker = true; const showOverflowMenu = showConnectorPicker || + showKnowledgePicker || showSkillPicker || showExpertPicker || showShortcutPicker; const overflowBadgeCount = selectedConnectors.length + + selectedKnowledgeBaseIds.length + selectedSkills.length + selectedTargetAgents.length; @@ -223,6 +244,7 @@ export default function ChatInputActionsRow({ const mobilePickerTitle: Record = { model: t("chat.selectModel", "Select model"), connector: t("connectors.chatPicker"), + knowledge: t("chat.knowledgePicker"), skill: t("chat.skillPicker"), expert: t("chat.expertPicker"), shortcut: t("shortcut.title", "快捷指令"), @@ -466,6 +488,26 @@ export default function ChatInputActionsRow({ )} + {showKnowledgePicker && ( + + )} {showSkillPicker && ( + + + )} {showSkillPicker && ( void; onSkillsChange?: (names: string[]) => void; onConnectorsChange?: (names: string[]) => void; + onKnowledgeBaseIdsChange?: (ids: string[]) => void; onTargetAgentsChange?: (ids: string[]) => void; onModelChange?: (model: string | null) => void; } +function ComposerImagePreview({ + url, + alt, + className, +}: { + url: string; + alt: string; + className?: string; +}) { + const { t } = useTranslation(); + const { src, loadState } = useAuthImageSrc(url, alt); + + if (loadState === "loading") { + return ( +
+ ); + } + + if (loadState === "error" || !src) { + return ( +
+ {t("chat.imageLoadFailed")} +
+ ); + } + + return ( + {alt} + ); +} + export default function ChatInputPreviewBar({ attachments, uploading, selectedSkills, selectedConnectors, + selectedKnowledgeBaseIds, selectedTargetAgents, availableSkills, availableConnectors, + availableKnowledgeBases, availableAgents, onRemoveAttachment, onSkillsChange, onConnectorsChange, + onKnowledgeBaseIdsChange, onTargetAgentsChange, selectedModel, onModelChange, @@ -60,34 +126,46 @@ export default function ChatInputPreviewBar({ attachments.length > 0 || uploading || selectedConnectors.length > 0 || + selectedKnowledgeBaseIds.length > 0 || selectedSkills.length > 0 || selectedTargetAgents.length > 0 || showModelChip; if (!hasContent) return null; + const imageAttachments = attachments.filter( + (attachment) => attachment.kind === "image", + ); + return (
+ {imageAttachments.length > 0 ? ( + + {attachments.map((attachment, idx) => + attachment.kind === "image" ? ( +
+ + +
+ ) : null, + )} +
+ ) : null} {attachments.map((attachment, idx) => - attachment.kind === "image" ? ( -
- - -
- ) : ( + attachment.kind === "image" ? null : (
); })} + {availableKnowledgeBases && + onKnowledgeBaseIdsChange && + selectedKnowledgeBaseIds.map((id) => { + const knowledgeBase = availableKnowledgeBases.find( + (base) => base.id === id, + ); + if (!knowledgeBase) return null; + return ( + + onKnowledgeBaseIdsChange( + selectedKnowledgeBaseIds.filter((baseId) => baseId !== id), + ) + } + /> + ); + })} {onTargetAgentsChange && selectedTargetAgents.map((id) => { const a = availableAgents.find((x) => x.agent_id === id); diff --git a/dashboard/src/pages/Chat/components/ContextChip.tsx b/dashboard/src/pages/Chat/components/ContextChip.tsx index fa71fc4f..b815f4e8 100644 --- a/dashboard/src/pages/Chat/components/ContextChip.tsx +++ b/dashboard/src/pages/Chat/components/ContextChip.tsx @@ -2,11 +2,17 @@ import type { ReactNode } from "react"; import { X } from "lucide-react"; import styles from "../index.module.less"; -export type ContextChipVariant = "skill" | "connector" | "expert" | "model"; +export type ContextChipVariant = + | "skill" + | "connector" + | "knowledge" + | "expert" + | "model"; const variantClass: Record = { skill: styles.contextChipSkill, connector: styles.contextChipConnector, + knowledge: styles.contextChipKnowledge, expert: styles.contextChipExpert, model: styles.contextChipModel, }; diff --git a/dashboard/src/pages/Chat/components/ExpertAgentAvatar.tsx b/dashboard/src/pages/Chat/components/ExpertAgentAvatar.tsx index 1167728c..fd9585ef 100644 --- a/dashboard/src/pages/Chat/components/ExpertAgentAvatar.tsx +++ b/dashboard/src/pages/Chat/components/ExpertAgentAvatar.tsx @@ -6,6 +6,9 @@ export interface ChatAgentOption { name: string; icon_name?: string | null; color?: string | null; + is_shared?: boolean; + is_owner?: boolean; + owner_username?: string | null; } interface ExpertAgentAvatarProps { diff --git a/dashboard/src/pages/Chat/components/ExpertPickerPopover.tsx b/dashboard/src/pages/Chat/components/ExpertPickerPopover.tsx index 57813aab..e253a2bf 100644 --- a/dashboard/src/pages/Chat/components/ExpertPickerPopover.tsx +++ b/dashboard/src/pages/Chat/components/ExpertPickerPopover.tsx @@ -2,10 +2,12 @@ import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { GraduationCap } from "lucide-react"; +import { Tag } from "antd"; import SearchablePickerPanel, { pickerStyles, } from "../../../components/ChatPicker/SearchablePickerPanel"; import ExpertAgentAvatar, { type ChatAgentOption } from "./ExpertAgentAvatar"; +import { isSharedExpertViewer } from "../../../utils/sharedExpert"; import styles from "../index.module.less"; export type { ChatAgentOption }; @@ -70,6 +72,15 @@ export default function ExpertPickerPopover({ /> {agent.name} + {agent.is_shared && ( + + {isSharedExpertViewer(agent) + ? t("experts.share.fromOwner", { + name: agent.owner_username, + }) + : t("experts.share.badge")} + + )} ); diff --git a/dashboard/src/pages/Chat/components/KnowledgePickerPopover.tsx b/dashboard/src/pages/Chat/components/KnowledgePickerPopover.tsx new file mode 100644 index 00000000..192f9548 --- /dev/null +++ b/dashboard/src/pages/Chat/components/KnowledgePickerPopover.tsx @@ -0,0 +1,97 @@ +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Switch } from "antd"; +import { Settings2 } from "lucide-react"; +import SearchablePickerPanel, { + pickerStyles, +} from "../../../components/ChatPicker/SearchablePickerPanel"; +import type { KnowledgeBase } from "../../../api/modules/knowledgeBases"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; +import { knowledgeIconForName } from "../../KnowledgeBases/knowledgeIcons"; +import styles from "../index.module.less"; + +interface KnowledgePickerPopoverProps { + knowledgeBases: KnowledgeBase[]; + selectedKnowledgeBaseIds: string[]; + onKnowledgeBaseIdsChange: (ids: string[]) => void; + onNavigateAway?: () => void; +} + +export default function KnowledgePickerPopover({ + knowledgeBases, + selectedKnowledgeBaseIds, + onKnowledgeBaseIdsChange, + onNavigateAway, +}: KnowledgePickerPopoverProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const currentUserId = useCurrentUser()?.id ?? null; + + const filterFn = useCallback( + (knowledgeBase: KnowledgeBase, query: string) => + knowledgeBase.name.toLowerCase().includes(query) || + knowledgeBase.description.toLowerCase().includes(query), + [], + ); + + return ( + } + footerLabel={t("chat.manageKnowledgeBases")} + onFooterClick={() => { + onNavigateAway?.(); + navigate("/knowledge-bases"); + }} + renderItem={(knowledgeBase) => { + const active = selectedKnowledgeBaseIds.includes(knowledgeBase.id); + return ( +
+ + {knowledgeIconForName(knowledgeBase.icon_name, 16)} + + + + {knowledgeBase.name} + + {knowledgeBase.default_open && + currentUserId != null && + knowledgeBase.owner_user_id === currentUserId ? ( + + {t("knowledgeBases.defaultOpenBadge")} + + ) : knowledgeBase.description ? ( + + {knowledgeBase.description} + + ) : null} + + { + const next = checked + ? [...selectedKnowledgeBaseIds, knowledgeBase.id] + : selectedKnowledgeBaseIds.filter( + (id) => id !== knowledgeBase.id, + ); + onKnowledgeBaseIdsChange(next); + }} + /> +
+ ); + }} + /> + ); +} diff --git a/dashboard/src/pages/Chat/components/MessageBubble.tsx b/dashboard/src/pages/Chat/components/MessageBubble.tsx index 0821caa1..2451e605 100644 --- a/dashboard/src/pages/Chat/components/MessageBubble.tsx +++ b/dashboard/src/pages/Chat/components/MessageBubble.tsx @@ -11,6 +11,7 @@ import { Pencil, Volume2, Settings, + GitBranch, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -53,6 +54,9 @@ interface MessageBubbleProps { composerLookups?: ComposerTagLookups; onRegenerate?: (messageId: string) => void; onEditUserMessage?: (messageId: string, newText: string) => void; + onForkUserMessage?: (messageId: string) => void; + forkDisabled?: boolean; + forkDisabledHint?: string; onHitlDecision?: ( decisions: Array<{ type: string; message?: string }>, ) => void; @@ -493,6 +497,9 @@ function MessageBubble({ composerLookups, onRegenerate, onEditUserMessage, + onForkUserMessage, + forkDisabled, + forkDisabledHint, onHitlDecision, compact, groupPosition = "only", @@ -724,7 +731,9 @@ function MessageBubble({ )} {message.content &&
{message.content}
}
- {(message.content || onEditUserMessage) && ( + {(message.content || + onEditUserMessage || + onForkUserMessage) && (
{message.content ? ( @@ -743,6 +752,22 @@ function MessageBubble({ ) : null} + {onForkUserMessage ? ( + + ) : null}
)}
diff --git a/dashboard/src/pages/Chat/components/MessageList.tsx b/dashboard/src/pages/Chat/components/MessageList.tsx index fd1f1d48..8146dade 100644 --- a/dashboard/src/pages/Chat/components/MessageList.tsx +++ b/dashboard/src/pages/Chat/components/MessageList.tsx @@ -96,6 +96,9 @@ interface MessageListProps { onCancel?: () => void; onRegenerate?: (messageId: string) => void; onEditUserMessage?: (messageId: string, newText: string) => void; + onForkUserMessage?: (messageId: string) => void; + forkDisabled?: boolean; + forkDisabledHint?: string; onAcpPermissionSelect?: (message: string) => void; onHitlDecision?: ( decisions: Array<{ type: string; message?: string }>, @@ -117,6 +120,9 @@ interface GroupRenderContext { lastUserGroupIndex: number; onRegenerate?: (messageId: string) => void; onEditUserMessage?: (messageId: string, newText: string) => void; + onForkUserMessage?: (messageId: string) => void; + forkDisabled?: boolean; + forkDisabledHint?: string; onAcpPermissionSelect?: (message: string) => void; onHitlDecision?: ( decisions: Array<{ type: string; message?: string }>, @@ -185,6 +191,9 @@ function renderMessageGroup( composerLookups={ctx.composerLookups} onRegenerate={ctx.onRegenerate} onEditUserMessage={ctx.onEditUserMessage} + onForkUserMessage={ctx.onForkUserMessage} + forkDisabled={ctx.forkDisabled} + forkDisabledHint={ctx.forkDisabledHint} />
); @@ -236,6 +245,9 @@ export default function MessageList(props: MessageListProps) { onCancel, onRegenerate, onEditUserMessage, + onForkUserMessage, + forkDisabled, + forkDisabledHint, onAcpPermissionSelect, onHitlDecision, onOpenBrowser, @@ -661,6 +673,9 @@ export default function MessageList(props: MessageListProps) { lastUserGroupIndex, onRegenerate, onEditUserMessage, + onForkUserMessage, + forkDisabled, + forkDisabledHint, onAcpPermissionSelect, onHitlDecision, onOpenBrowser, @@ -680,6 +695,9 @@ export default function MessageList(props: MessageListProps) { lastUserGroupIndex, onRegenerate, onEditUserMessage, + onForkUserMessage, + forkDisabled, + forkDisabledHint, onAcpPermissionSelect, onHitlDecision, onOpenBrowser, diff --git a/dashboard/src/pages/Chat/components/SessionList.tsx b/dashboard/src/pages/Chat/components/SessionList.tsx index b3d94b6a..8b2be3ec 100644 --- a/dashboard/src/pages/Chat/components/SessionList.tsx +++ b/dashboard/src/pages/Chat/components/SessionList.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useMemo, useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { Dropdown } from "antd"; +import { Dropdown, Tooltip } from "antd"; import type { MenuProps } from "antd"; import { Pencil, @@ -10,10 +10,12 @@ import { Pin, PinOff, Search, + Users, } from "lucide-react"; import type { Session } from "../hooks/useSessions"; import type { OctopAgent } from "../../../context/AgentContext"; import { isAgentChatReady } from "../../../utils/agentError"; +import { isSharedExpertViewer } from "../../../utils/sharedExpert"; import { showConfirmModal } from "../../../utils/confirmModal"; import { iconForName } from "../../Experts/components/iconForName"; import SessionChannelIcon from "./SessionChannelIcon"; @@ -32,6 +34,25 @@ function AgentUnreadBadge({ count }: { count: number }) { ); } +function SharedExpertHint({ agent }: { agent: OctopAgent }) { + const { t } = useTranslation(); + if (!isSharedExpertViewer(agent)) return null; + const tip = t("chat.sharedExpert.banner", { + name: agent.owner_username || "—", + }); + return ( + + event.stopPropagation()} + > + + + + ); +} + interface SessionItemProps { session: Session; isActive: boolean; @@ -257,6 +278,7 @@ function ActiveAgentCard({
{agent.name}
+
{agent.description ? ( @@ -333,6 +355,7 @@ function InactiveAgentRow({ agent, onSelect }: AgentRowProps) {
{agent.name}
+
{agent.description || "—"}
diff --git a/dashboard/src/pages/Chat/components/UserMessageComposerTags.tsx b/dashboard/src/pages/Chat/components/UserMessageComposerTags.tsx index bfbaf2f6..782420fb 100644 --- a/dashboard/src/pages/Chat/components/UserMessageComposerTags.tsx +++ b/dashboard/src/pages/Chat/components/UserMessageComposerTags.tsx @@ -3,9 +3,11 @@ import { Cpu } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { UserComposerContext } from "../hooks/useChat"; import type { SkillSpec } from "../../Agent/Skills/useSkills"; +import type { KnowledgeBase } from "../../../api/modules/knowledgeBases"; import type { ChatAgentOption } from "./ExpertAgentAvatar"; import ExpertAgentAvatar from "./ExpertAgentAvatar"; import { ConnectorLogo } from "../../Agent/Connectors/connectorDefs"; +import { knowledgeIconForName } from "../../KnowledgeBases/knowledgeIcons"; import ContextChip, { type ContextChipVariant } from "./ContextChip"; import { skillChipLabel } from "../utils/skillChipLabel"; import { modelShortLabel } from "../../../utils/modelOptions"; @@ -14,6 +16,7 @@ import styles from "../index.module.less"; export interface ComposerTagLookups { skills?: SkillSpec[]; connectors?: { mcp_server_name: string; label: string; kind: string }[]; + knowledgeBases?: KnowledgeBase[]; agents?: ChatAgentOption[]; } @@ -64,6 +67,18 @@ export default function UserMessageComposerTags({ }); } + for (const id of context.knowledgeBaseIds ?? []) { + const knowledgeBase = lookups?.knowledgeBases?.find( + (base) => base.id === id, + ); + items.push({ + key: `knowledge-${id}`, + variant: "knowledge", + icon: knowledgeIconForName(knowledgeBase?.icon_name, 11), + label: knowledgeBase?.name || id, + }); + } + for (const id of context.targetAgents ?? []) { const agent = lookups?.agents?.find((a) => a.agent_id === id); items.push({ diff --git a/dashboard/src/pages/Chat/hooks/chatStore.ts b/dashboard/src/pages/Chat/hooks/chatStore.ts index cffd0074..522e5600 100644 --- a/dashboard/src/pages/Chat/hooks/chatStore.ts +++ b/dashboard/src/pages/Chat/hooks/chatStore.ts @@ -123,6 +123,24 @@ export function consumePendingPrefillText(): string { return val; } +let _pendingPrefillAttachments: ChatAttachment[] = []; + +/** Enqueue attachments to restore in the composer on the next Chat mount / thread switch. */ +export function setPendingPrefillAttachments( + attachments: ChatAttachment[], +): void { + _pendingPrefillAttachments = attachments.map((attachment) => ({ + ...attachment, + })); +} + +/** Consume pending prefill attachments (clears after reading). */ +export function consumePendingPrefillAttachments(): ChatAttachment[] { + const val = _pendingPrefillAttachments; + _pendingPrefillAttachments = []; + return val.map((attachment) => ({ ...attachment })); +} + // ── Composer draft (sessionStorage) ─────────────────────────────────────── // Survives navigating away from /chat and back; keyed per agent + thread. @@ -1662,6 +1680,7 @@ async function sendTurnWebSocket( modelRef?: string | null, threadId?: string | null, mcpServers?: string[] | null, + knowledgeBaseIds?: string[] | null, skills?: string[] | null, targetAgentIds?: string[] | null, onStreamEnd?: () => void, @@ -1734,6 +1753,11 @@ async function sendTurnWebSocket( if (mcpServers !== undefined && mcpServers !== null) { payload.mcp_servers = mcpServers; } + // Always send the array (including []) so the server can honor Dashboard + // opt-out of default_open knowledge bases for this turn. + if (knowledgeBaseIds !== undefined && knowledgeBaseIds !== null) { + payload.knowledge_base_ids = knowledgeBaseIds; + } if (skills && skills.length > 0) payload.skills = skills; if (targetAgentIds && targetAgentIds.length > 0) { payload.target_agent_ids = targetAgentIds; @@ -1859,6 +1883,7 @@ export async function sendTurn( modelRef?: string | null, threadId?: string | null, mcpServers?: string[] | null, + knowledgeBaseIds?: string[] | null, skills?: string[] | null, targetAgentIds?: string[] | null, reasoningMode?: "auto" | "enabled" | "disabled", @@ -1917,6 +1942,7 @@ export async function sendTurn( modelRef, threadId, mcpServers, + knowledgeBaseIds, skills, targetAgentIds, onStreamEnd, diff --git a/dashboard/src/pages/Chat/hooks/sseHelpers.ts b/dashboard/src/pages/Chat/hooks/sseHelpers.ts index fe8ff98f..00351d2f 100644 --- a/dashboard/src/pages/Chat/hooks/sseHelpers.ts +++ b/dashboard/src/pages/Chat/hooks/sseHelpers.ts @@ -42,10 +42,11 @@ export interface ChatAttachment { kind: "image" | "file"; } -/** Skills, connectors, experts, and selected model attached at send time. */ +/** Skills, connectors, knowledge bases, experts, and selected model attached at send time. */ export interface UserComposerContext { skills?: string[]; connectors?: string[]; + knowledgeBaseIds?: string[]; targetAgents?: string[]; model?: string; reasoningMode?: "auto" | "enabled" | "disabled"; diff --git a/dashboard/src/pages/Chat/hooks/useChat.ts b/dashboard/src/pages/Chat/hooks/useChat.ts index 4f103be3..50d60181 100644 --- a/dashboard/src/pages/Chat/hooks/useChat.ts +++ b/dashboard/src/pages/Chat/hooks/useChat.ts @@ -645,6 +645,7 @@ export function useChat( storeKey?: string, modelRef?: string | null, mcpServers?: string[] | null, + knowledgeBaseIds?: string[] | null, skills?: string[] | null, targetAgentIds?: string[] | null, composerContext?: UserComposerContext, @@ -678,6 +679,7 @@ export function useChat( modelRef, threadIdForApi, mcpServers, + knowledgeBaseIds, skills, targetAgentIds, reasoningMode, diff --git a/dashboard/src/pages/Chat/hooks/useChatComposerResources.ts b/dashboard/src/pages/Chat/hooks/useChatComposerResources.ts index fb04d1e7..4a7fec9b 100644 --- a/dashboard/src/pages/Chat/hooks/useChatComposerResources.ts +++ b/dashboard/src/pages/Chat/hooks/useChatComposerResources.ts @@ -4,9 +4,14 @@ import { providerApi } from "../../../api/modules/provider"; import { preferencesApi } from "../../../api/modules/preferences"; import { octopThreadsApi } from "../../../api/modules/octopThreads"; import { request } from "../../../api/request"; +import { + knowledgeBasesApi, + type KnowledgeBase, +} from "../../../api/modules/knowledgeBases"; import type { ResolvedModel } from "../../../api/types"; import type { SkillSpec } from "../../Agent/Skills/useSkills"; import { CONNECTORS_CHANGED_EVENT } from "../../Agent/Connectors/customMcpUtils"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; import { activeModelToRef } from "./useChatContextWindow"; import { hasSavedConnectors, @@ -16,6 +21,7 @@ import { saveSkills, } from "../utils/chatStorage"; import { resolveInitialConnectors } from "../utils/resolveInitialConnectors"; +import { withDefaultOpenKnowledgeBases } from "../utils/withDefaultOpenKnowledgeBases"; export function useChatComposerResources( resolvedAgentId: string | null | undefined, @@ -25,8 +31,16 @@ export function useChatComposerResources( stickyReasoningMode?: "auto" | "enabled" | "disabled" | null, stickyReasoningEffort?: string | null, ) { + const user = useCurrentUser(); + const currentUserId = user?.id ?? null; const [selectedConnectors, setSelectedConnectors] = useState([]); const [selectedSkills, setSelectedSkills] = useState([]); + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState< + string[] + >([]); + const [chatKnowledgeBases, setChatKnowledgeBases] = useState< + KnowledgeBase[] | undefined + >(undefined); const [chatConnectors, setChatConnectors] = useState< { mcp_server_name: string; @@ -167,6 +181,38 @@ export function useChatComposerResources( }); }, [resolvedAgentId, chatSkills]); + useEffect(() => { + let cancelled = false; + setSelectedKnowledgeBaseIds([]); + setChatKnowledgeBases(undefined); + void knowledgeBasesApi + .getCapability() + .then((capability) => { + if (cancelled || !capability.usable) return; + return knowledgeBasesApi.list().then((bases) => { + if (cancelled) return; + setChatKnowledgeBases(bases); + const ownedDefaults = bases + .filter( + (base) => + base.default_open && + currentUserId != null && + base.owner_user_id === currentUserId, + ) + .map((base) => base.id); + setSelectedKnowledgeBaseIds((previous) => + withDefaultOpenKnowledgeBases(previous, ownedDefaults), + ); + }); + }) + .catch(() => { + if (!cancelled) setChatKnowledgeBases(undefined); + }); + return () => { + cancelled = true; + }; + }, [resolvedAgentId, currentUserId]); + useEffect(() => { let cancelled = false; const loadModels = () => { @@ -241,6 +287,10 @@ export function useChatComposerResources( [resolvedAgentId], ); + const handleKnowledgeBaseIdsChange = useCallback((ids: string[]) => { + setSelectedKnowledgeBaseIds(ids); + }, []); + const handleModelChange = useCallback( (model: string | null) => { setSelectedModel(model); @@ -305,10 +355,13 @@ export function useChatComposerResources( handleReasoningChange, selectedConnectors, selectedSkills, + selectedKnowledgeBaseIds, chatConnectors, + chatKnowledgeBases, availableModels, activeModelRef, handleConnectorsChange, handleSkillsChange, + handleKnowledgeBaseIdsChange, }; } diff --git a/dashboard/src/pages/Chat/hooks/useChatSend.ts b/dashboard/src/pages/Chat/hooks/useChatSend.ts index 9f5a5a7f..b16ed378 100644 --- a/dashboard/src/pages/Chat/hooks/useChatSend.ts +++ b/dashboard/src/pages/Chat/hooks/useChatSend.ts @@ -21,6 +21,7 @@ interface UseChatSendParams { messagesLength: number; selectedModel: string | null; selectedConnectors: string[]; + selectedKnowledgeBaseIds: string[]; selectedSkills: string[]; selectedTargetAgents: string[]; reasoningMode: "auto" | "enabled" | "disabled"; @@ -34,6 +35,7 @@ interface UseChatSendParams { storeKey?: string, modelRef?: string | null, mcpServers?: string[] | null, + knowledgeBaseIds?: string[] | null, skills?: string[] | null, targetAgentIds?: string[] | null, composerContext?: UserComposerContext, @@ -55,6 +57,7 @@ function deriveThreadTitle(msg: string): string { export type ChatSendOverrides = { selectedModel?: string | null; selectedConnectors?: string[]; + selectedKnowledgeBaseIds?: string[]; selectedSkills?: string[]; selectedTargetAgents?: string[]; composerContext?: UserComposerContext; @@ -72,6 +75,7 @@ export function useChatSend({ messagesLength, selectedModel, selectedConnectors, + selectedKnowledgeBaseIds, selectedSkills, selectedTargetAgents, reasoningMode, @@ -110,6 +114,8 @@ export function useChatSend({ const skills = overrides?.selectedSkills ?? selectedSkills; const connectors = overrides?.selectedConnectors ?? selectedConnectors; + const knowledgeBaseIds = + overrides?.selectedKnowledgeBaseIds ?? selectedKnowledgeBaseIds; const targetAgents = overrides?.selectedTargetAgents ?? selectedTargetAgents; const modelSelection = @@ -122,6 +128,7 @@ export function useChatSend({ buildComposerContext({ skills, connectors, + knowledgeBaseIds, targetAgents, selectedModel: modelSelection, reasoningMode: @@ -145,6 +152,7 @@ export function useChatSend({ tid, modelOverride, connectors, + knowledgeBaseIds, skills, targetAgents, composerContext, @@ -198,6 +206,7 @@ export function useChatSend({ modelOverride, tid, connectors, + knowledgeBaseIds, skills, targetAgents, composerContext?.reasoningMode ?? reasoningMode, @@ -218,6 +227,7 @@ export function useChatSend({ resolvedAgentId, selectedModel, selectedConnectors, + selectedKnowledgeBaseIds, selectedSkills, selectedTargetAgents, reasoningMode, diff --git a/dashboard/src/pages/Chat/index.tsx b/dashboard/src/pages/Chat/index.tsx index 2c5ad59c..36b622f1 100644 --- a/dashboard/src/pages/Chat/index.tsx +++ b/dashboard/src/pages/Chat/index.tsx @@ -13,11 +13,12 @@ import { Tooltip } from "antd"; import { message as antMessage } from "@/utils/antdMessage"; import { useIsMobile } from "../../hooks/useIsMobile"; -import { useUserRole } from "../../hooks/useUserRole"; +import { useCurrentUser } from "../../hooks/useCurrentUser"; +import { userCan } from "../../utils/permissions"; import { useChat } from "./hooks/useChat"; import { useSessions } from "./hooks/useSessions"; import * as chatStore from "./hooks/chatStore"; -import { formatRunUsage } from "./utils/chatMessages"; +import { formatRunUsage, userTurnsFromEnd } from "./utils/chatMessages"; import { useChatSidebarState } from "./hooks/useChatSidebarState"; import { useChatHistoryRail } from "./hooks/useChatHistoryRail"; import { useChatDockPanel } from "./hooks/useChatDockPanel"; @@ -37,6 +38,7 @@ import { useChatFileDetection } from "./hooks/useChatFileDetection"; import { useSkillRecordingWorkflow } from "./hooks/useSkillRecordingWorkflow"; import { dedupeDockFilePaths } from "./utils/dockFilePath"; import { browserApi } from "../../api/modules/browser"; +import { octopThreadsApi } from "../../api/modules/octopThreads"; import type { TokenUsage } from "../../api/types"; import type { ChatAttachment } from "./hooks/useChat"; import MessageList from "./components/MessageList"; @@ -49,12 +51,14 @@ import { useSkills } from "../Agent/Skills/useSkills"; import { useAgent } from "../../context/AgentContext"; import { useBrowserSessionState } from "../../hooks/useBrowserSessionState"; import { prefetchVoiceConfig } from "../../hooks/useVoiceConfig"; +import { isSharedExpertViewer } from "../../utils/sharedExpert"; import ChatDockPanels from "./components/ChatDockPanels"; import { ChatFilePreviewProvider } from "./ChatFilePreviewContext"; import ChatSidebarPanel from "./components/ChatSidebarPanel"; import ChatTitleBar from "./components/ChatTitleBar"; import ChatComposerChrome from "./components/ChatComposerChrome"; import { isAgentChatReady } from "../../utils/agentError"; +import { apiErrorMessage } from "../../utils/apiError"; import PwaInstallPrompt from "../../components/PwaInstallPrompt"; import { promptNeedsUserInput } from "../../utils/quickInputPrefill"; import styles from "./index.module.less"; @@ -73,8 +77,8 @@ function ChatPageInner() { threadId?: string; }>(); const isMobile = useIsMobile(); - const role = useUserRole(); - const isAdmin = role === "admin"; + const user = useCurrentUser(); + const canTerminal = userCan(user, "terminal"); const chatHistoryRail = useChatHistoryRail(); const [selectedTargetAgents, setSelectedTargetAgents] = useState( [], @@ -147,6 +151,7 @@ function ChatPageInner() { [agents, resolvedAgentId], ); const agentChatReady = isAgentChatReady(activeAgent?.state); + const sharedExpertViewer = isSharedExpertViewer(activeAgent ?? {}); const noAgents = !agentsLoading && agents.length === 0; useEffect(() => { @@ -156,7 +161,9 @@ function ChatPageInner() { const { quickCards: expertQuickCards, welcomeSuffix } = useExpertChatWelcome(activeAgent); const { skills: chatSkills } = useSkills( - agentChatReady && !agentsLoading ? resolvedAgentId ?? null : null, + agentChatReady && !agentsLoading && !sharedExpertViewer + ? resolvedAgentId ?? null + : null, ); const [agentProfileOpen, setAgentProfileOpen] = useState(false); @@ -289,7 +296,9 @@ function ChatPageInner() { setSelectedModel, selectedConnectors, selectedSkills, + selectedKnowledgeBaseIds, chatConnectors, + chatKnowledgeBases, availableModels, activeModelRef, reasoningMode, @@ -297,6 +306,7 @@ function ChatPageInner() { handleReasoningChange, handleConnectorsChange, handleSkillsChange, + handleKnowledgeBaseIdsChange, } = useChatComposerResources( resolvedAgentId, chatSkills, @@ -364,6 +374,9 @@ function ChatPageInner() { name: a.name, icon_name: a.icon_name, color: a.color, + is_shared: a.is_shared, + is_owner: a.is_owner, + owner_username: a.owner_username, })), [agents], ); @@ -372,9 +385,10 @@ function ChatPageInner() { () => ({ skills: chatSkills, connectors: chatConnectors, + knowledgeBases: chatKnowledgeBases, agents: chatAgentOptions, }), - [chatSkills, chatConnectors, chatAgentOptions], + [chatSkills, chatConnectors, chatKnowledgeBases, chatAgentOptions], ); const { handleSend } = useChatSend({ @@ -384,6 +398,7 @@ function ChatPageInner() { messagesLength: messages.length, selectedModel, selectedConnectors, + selectedKnowledgeBaseIds, selectedSkills, selectedTargetAgents, reasoningMode, @@ -442,6 +457,7 @@ function ChatPageInner() { selectedModel: item.composerContext?.model ?? item.modelRef ?? null, selectedSkills: item.composerContext?.skills, selectedConnectors: item.composerContext?.connectors, + selectedKnowledgeBaseIds: item.composerContext?.knowledgeBaseIds, selectedTargetAgents: item.composerContext?.targetAgents, threadId: ctx.threadId, agentId: ctx.agentId || undefined, @@ -575,6 +591,87 @@ function ChatPageInner() { [activeThreadId, editAndResend, resolvedAgentId], ); + const [forking, setForking] = useState(false); + const hasPendingHitl = useMemo( + () => messages.some((message) => message.hitlData?.status === "pending"), + [messages], + ); + const forkDisabled = forking || isStreaming || hasPendingHitl; + const forkDisabledHint = + !forking && (isStreaming || hasPendingHitl) + ? t("chat.forkDisabledWhileBusy") + : undefined; + const handleForkUserMessage = useCallback( + async (messageId: string) => { + const agent = resolvedAgentId; + if (!agent || !activeThreadId || forkDisabled) return; + const idx = messages.findIndex((message) => message.id === messageId); + if (idx < 0) return; + const userMsg = messages[idx]; + if (userMsg.role !== "user") return; + const turnsFromEnd = userTurnsFromEnd(messages, messageId); + if (turnsFromEnd < 1) return; + setForking(true); + try { + const created = await octopThreadsApi.fork(agent, activeThreadId, { + message_id: messageId, + content: userMsg.content, + user_turns_from_end: turnsFromEnd, + }); + const ctx = userMsg.composerContext; + if (ctx?.skills) handleSkillsChange(ctx.skills); + if (ctx?.connectors) handleConnectorsChange(ctx.connectors); + if (ctx?.knowledgeBaseIds) { + handleKnowledgeBaseIdsChange(ctx.knowledgeBaseIds); + } + if (ctx?.targetAgents) setSelectedTargetAgents(ctx.targetAgents); + if (ctx?.model) setSelectedModel(ctx.model); + if (ctx?.reasoningMode) { + handleReasoningChange(ctx.reasoningMode, ctx.reasoningEffort ?? null); + } + const forkAttachments = userMsg.attachments?.map((attachment) => ({ + ...attachment, + })); + prefillInputRef.current = userMsg.content; + if (forkAttachments && forkAttachments.length > 0) { + chatStore.setPendingPrefillAttachments(forkAttachments); + } + chatInputRef.current?.setPrefillComposer( + userMsg.content, + forkAttachments, + ); + await ensureThreadInList(created.thread_id); + navigate(`/chat/${agent}/${created.thread_id}`, { + state: { prefillInput: userMsg.content }, + }); + antMessage.success( + created.copied_messages > 0 + ? t("chat.forkSuccess") + : t("chat.forkSuccessEmpty"), + ); + } catch (error) { + antMessage.error(apiErrorMessage(error, t("chat.forkFailed"), t)); + } finally { + setForking(false); + } + }, + [ + activeThreadId, + forkDisabled, + handleConnectorsChange, + handleKnowledgeBaseIdsChange, + handleReasoningChange, + handleSkillsChange, + messages, + navigate, + resolvedAgentId, + setSelectedModel, + setSelectedTargetAgents, + t, + ensureThreadInList, + ], + ); + const hasMessages = messages.length > 0; // On hard refresh / deep-link into a thread, messages start empty. Showing // Welcome until history returns looks like a full page flash. Keep the list @@ -662,7 +759,7 @@ function ChatPageInner() { {activeSessionTitle}
)} - {resolvedAgentId && ( + {resolvedAgentId && !sharedExpertViewer && (
+ ); + + return ( + <> + {published ? ( + + + {iconButton} + + + ) : ( + + {iconButton} + + )} + + setDrawerOpen(false)} + onSuccess={onChanged} + /> + + ); +} diff --git a/dashboard/src/pages/Experts/components/PublishedExpertCard.tsx b/dashboard/src/pages/Experts/components/PublishedExpertCard.tsx new file mode 100644 index 00000000..c5f300b4 --- /dev/null +++ b/dashboard/src/pages/Experts/components/PublishedExpertCard.tsx @@ -0,0 +1,117 @@ +import { memo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Dropdown, Modal } from "antd"; +import { message } from "@/utils/antdMessage"; +import { MoreHorizontal } from "lucide-react"; +import { iconForName } from "./iconForName"; +import { + publishedExpertsApi, + type PublishedExpert, +} from "../../../api/modules/publishedExperts"; +import { apiErrorMessage } from "../../../utils/apiError"; +import styles from "../index.module.less"; + +interface PublishedExpertCardProps { + expert: PublishedExpert; + canManage: boolean; + onInstall: (expert: PublishedExpert) => void; + onChanged: () => void; +} + +export const PublishedExpertCard = memo(function PublishedExpertCard({ + expert, + canManage, + onInstall, + onChanged, +}: PublishedExpertCardProps) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const accent = expert.color || "var(--fn-color-brand)"; + + const confirmUnpublish = () => { + Modal.confirm({ + title: t("experts.published.unpublishConfirm"), + okText: t("experts.published.unpublish"), + cancelText: t("common.cancel"), + okButtonProps: { danger: true }, + onOk: async () => { + setLoading(true); + try { + await publishedExpertsApi.unpublish(expert.id); + message.success(t("experts.published.unpublishSuccess")); + onChanged(); + } catch (err) { + message.error( + apiErrorMessage(err, t("experts.published.unpublishFailed"), t), + ); + } finally { + setLoading(false); + } + }, + }); + }; + + return ( +
onInstall(expert)} + style={{ "--expert-accent": accent } as React.CSSProperties} + > +
+
+ {iconForName(expert.icon_name, 20)} +
+
+
{expert.name}
+
+ {t("experts.published.badge")} +
+
+ {canManage && ( + { + domEvent.stopPropagation(); + confirmUnpublish(); + }, + }, + ], + }} + trigger={["click"]} + disabled={loading} + > + + + )} +
+
+ {expert.description || "\u00a0"} +
+
+
+ {t("experts.published.install")} +
+ {expert.creator_username && ( +
+ {t("experts.published.by", { name: expert.creator_username })} +
+ )} +
+
+ ); +}); diff --git a/dashboard/src/pages/Experts/components/RootDirSelect.tsx b/dashboard/src/pages/Experts/components/RootDirSelect.tsx index 9bba7bc2..f6a977a2 100644 --- a/dashboard/src/pages/Experts/components/RootDirSelect.tsx +++ b/dashboard/src/pages/Experts/components/RootDirSelect.tsx @@ -75,6 +75,8 @@ export default function RootDirSelect({ value, onChange }: RootDirSelectProps) { const [editingName, setEditingName] = useState(""); const [busy, setBusy] = useState(false); const [open, setOpen] = useState(false); + const [loadedKeys, setLoadedKeys] = useState([]); + const loadingPathsRef = useRef(new Set()); useEffect(() => { if (!value) return; @@ -84,8 +86,15 @@ export default function RootDirSelect({ value, onChange }: RootDirSelectProps) { const loadData = useCallback>( async (node) => { const path = String(node.value ?? ""); - if (!path) return; + if ( + !path || + loadedKeys.includes(path) || + loadingPathsRef.current.has(path) + ) { + return; + } + loadingPathsRef.current.add(path); try { const data = await request<{ entries: DirEntry[] }>( `/filesystem/dirs?path=${encodeURIComponent(path)}`, @@ -98,11 +107,14 @@ export default function RootDirSelect({ value, onChange }: RootDirSelectProps) { setTreeData( withSanitizedTree((prev) => appendChildren(prev, path, children)), ); + setLoadedKeys((prev) => (prev.includes(path) ? prev : [...prev, path])); } catch { message.error(t("experts.rootDirListFailed")); + } finally { + loadingPathsRef.current.delete(path); } }, - [t], + [loadedKeys, t], ); const beginEditing = useCallback((path: string, name: string) => { @@ -137,6 +149,9 @@ export default function RootDirSelect({ value, onChange }: RootDirSelectProps) { renameNode(prev, path, result.path, result.name), ), ); + setLoadedKeys((prev) => + prev.map((key) => (key === path ? result.path : key)), + ); if (value === path) { onChange?.(result.path); } @@ -305,6 +320,8 @@ export default function RootDirSelect({ value, onChange }: RootDirSelectProps) { }} treeData={displayTreeData} loadData={loadData} + treeLoadedKeys={loadedKeys} + virtual={false} treeNodeLabelProp="value" treeExpandedKeys={expandedKeys} onTreeExpand={(keys) => setExpandedKeys(keys.map(String))} diff --git a/dashboard/src/pages/Experts/components/SubagentDrawer.test.ts b/dashboard/src/pages/Experts/components/SubagentDrawer.test.ts new file mode 100644 index 00000000..33884daf --- /dev/null +++ b/dashboard/src/pages/Experts/components/SubagentDrawer.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { buildSubagentMarkdown, parseSubagentForm } from "./SubagentDrawer"; +import { + expertPaletteColor, + resolveExpertPalette, +} from "../../../utils/expertColor"; +import { DEFAULT_PALETTE } from "../../../styles/themePalettes"; + +describe("SubagentDrawer color frontmatter", () => { + it("round-trips palette hex through markdown build/parse", () => { + const color = expertPaletteColor("tech"); + const md = buildSubagentMarkdown({ + slug: "helper", + name: "Helper", + description: "Helps", + emoji: "🧰", + color, + body: "Do the thing.", + }); + expect(md).toMatch(/color:\s*"?#4B74FA"?/); + expect(parseSubagentForm(md, "helper").color).toBe(color); + }); + + it("maps empty form color to default palette for the picker", () => { + expect(resolveExpertPalette("")).toBe(DEFAULT_PALETTE); + expect(resolveExpertPalette(undefined)).toBe(DEFAULT_PALETTE); + }); + + it("maps picker palette back to swatch hex for the form field", () => { + expect(expertPaletteColor(resolveExpertPalette("#4B74FA"))).toBe( + expertPaletteColor("tech"), + ); + }); +}); diff --git a/dashboard/src/pages/Experts/components/SubagentDrawer.tsx b/dashboard/src/pages/Experts/components/SubagentDrawer.tsx index 20b8e117..02ac7edf 100644 --- a/dashboard/src/pages/Experts/components/SubagentDrawer.tsx +++ b/dashboard/src/pages/Experts/components/SubagentDrawer.tsx @@ -3,7 +3,14 @@ import { Button, Drawer, Form, Input, Segmented, Spin } from "antd"; import { message } from "@/utils/antdMessage"; import type { FormInstance } from "antd"; import { useTranslation } from "react-i18next"; +import ExpertColorPicker from "../../../components/ExpertColorPicker"; +import EmojiPicker from "../../../components/EmojiPicker"; +import { + expertPaletteColor, + resolveExpertPalette, +} from "../../../utils/expertColor"; import { splitMarkdownFrontmatter } from "../../../utils/markdown"; +import { DEFAULT_PALETTE } from "../../../styles/themePalettes"; import styles from "./SubagentDrawer.module.less"; export interface SubagentFormValues { @@ -26,6 +33,22 @@ type EditorTab = "form" | "source"; export const SUBAGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]*$/; +/** Form-bound adapter: stores hex in the form, shows curated swatches. */ +function SubagentColorField({ + value, + onChange, +}: { + value?: string; + onChange?: (hex: string) => void; +}) { + return ( + onChange?.(expertPaletteColor(palette))} + /> + ); +} + function yamlQuote(value: string): string { if (!value) return '""'; if (/[:#\n"'{}[\],&*?|>!%@`]/.test(value) || value.trim() !== value) { @@ -125,7 +148,7 @@ export function SubagentDrawer({ name: "", description: "", emoji: "🤖", - color: "", + color: expertPaletteColor(DEFAULT_PALETTE), body: t("subagents.newBodyTemplate"), content: "", }); @@ -280,14 +303,19 @@ export function SubagentDrawer({ autoSize={{ minRows: 2, maxRows: 4 }} /> - - + + - - + +
diff --git a/dashboard/src/pages/Experts/components/SubagentManager.tsx b/dashboard/src/pages/Experts/components/SubagentManager.tsx index 432e0f5a..f7d29c1f 100644 --- a/dashboard/src/pages/Experts/components/SubagentManager.tsx +++ b/dashboard/src/pages/Experts/components/SubagentManager.tsx @@ -2,7 +2,13 @@ * SubagentManager — reusable subagent management content. * Used inside SubagentCatalogDrawer (Drawer) and Subagents page. */ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + type CSSProperties, +} from "react"; import { Alert, Empty, Form, Input, Modal, Spin, Tabs } from "antd"; import { message } from "@/utils/antdMessage"; @@ -22,6 +28,10 @@ import { request } from "../../../api/request"; import { workspaceApi } from "../../../api/modules/workspace"; import { apiErrorMessage } from "../../../utils/apiError"; import { isAgentChatReady } from "../../../utils/agentError"; +import { + resolveSubagentAccent, + subagentAccentIconStyle, +} from "../../../utils/expertColor"; import { withFromWorkspace } from "../../../utils/fromWorkspace"; import { normalizeUiLocale } from "../../../utils/locale"; import { pickLocale } from "../../../utils/localizedText"; @@ -360,13 +370,18 @@ export default function SubagentManager({
{filteredCatalogItems.map((item) => { const installed = installedSlugs.has(item.slug); - const accent = item.color?.startsWith("#") ? item.color : "#6366f1"; + const accent = resolveSubagentAccent(item.color); return ( -
+
+
{item.emoji ?? "🤖"}
@@ -462,13 +477,18 @@ export default function SubagentManager({ return (
{filteredInstalled.map((subagent) => { - const accent = "#6366f1"; + const accent = resolveSubagentAccent(subagent.color); return ( -
+
+
{subagent.emoji ?? "🤖"}
diff --git a/dashboard/src/pages/Experts/components/expertFileGroups.test.ts b/dashboard/src/pages/Experts/components/expertFileGroups.test.ts new file mode 100644 index 00000000..0ceadcb9 --- /dev/null +++ b/dashboard/src/pages/Experts/components/expertFileGroups.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { groupExpertFiles } from "./expertFileGroups"; + +describe("groupExpertFiles", () => { + it("separates config markdown, skills, and subagents", () => { + const { configFiles, skillGroups, subagentFiles } = groupExpertFiles([ + { name: "SOUL.md", content: "# Soul" }, + { name: "AGENTS.md", content: "# Agents guide" }, + { + name: "skills/demo/SKILL.md", + content: + '---\nname: Demo Skill\ndescription: A demo\nmetadata:\n octop:\n emoji: "🎯"\n---\n\n# Skill', + }, + { + name: "agents/reviewer.md", + content: + "---\nname: Reviewer\ndescription: Review code\nemoji: 🔍\n---\n\n# Body\n", + }, + ]); + + expect(configFiles.map((f) => f.name)).toEqual(["SOUL.md", "AGENTS.md"]); + expect(skillGroups).toHaveLength(1); + expect(skillGroups[0]?.name).toBe("demo"); + expect(skillGroups[0]?.emoji).toBe("🎯"); + expect(skillGroups[0]?.displayName).toBe("Demo Skill"); + expect(skillGroups[0]?.description).toBe("A demo"); + expect(subagentFiles).toHaveLength(1); + expect(subagentFiles[0]?.slug).toBe("reviewer"); + expect(subagentFiles[0]?.name).toBe("Reviewer"); + expect(subagentFiles[0]?.description).toBe("Review code"); + expect(subagentFiles[0]?.emoji).toBe("🔍"); + }); + + it("excludes manifest.json from config preview", () => { + const { configFiles } = groupExpertFiles([ + { name: "SOUL.md", content: "# Soul" }, + { name: "manifest.json", content: "{}" }, + ]); + expect(configFiles.map((f) => f.name)).toEqual(["SOUL.md"]); + }); +}); diff --git a/dashboard/src/pages/Experts/components/expertFileGroups.ts b/dashboard/src/pages/Experts/components/expertFileGroups.ts index 157055d9..2559f85d 100644 --- a/dashboard/src/pages/Experts/components/expertFileGroups.ts +++ b/dashboard/src/pages/Experts/components/expertFileGroups.ts @@ -1,5 +1,7 @@ import { request } from "../../../api/request"; import { withFromWorkspace } from "../../../utils/fromWorkspace"; +import { parseSkillPreviewFromMarkdown } from "../../Agent/Skills/skillMarkdown"; +import { parseSubagentForm } from "./SubagentDrawer"; export interface NamedFileContent { name: string; @@ -9,27 +11,84 @@ export interface NamedFileContent { export interface SkillFileGroup { name: string; files: NamedFileContent[]; + emoji: string; + displayName: string; + description: string; } -/** Split expert template files into root config md and skills/ tree. */ +export interface SubagentFilePreview { + slug: string; + name: string; + description: string; + emoji: string; + file: NamedFileContent; +} + +function isSubagentPath(name: string): boolean { + return name.startsWith("agents/") && name.endsWith(".md"); +} + +function isPromptMdPath(name: string): boolean { + return !name.includes("/") && name.endsWith(".md"); +} + +/** Split expert template files into persona md, skills/, and subagent definitions. */ export function groupExpertFiles(files: NamedFileContent[]): { configFiles: NamedFileContent[]; skillGroups: SkillFileGroup[]; + subagentFiles: SubagentFilePreview[]; } { - const configFiles = files.filter((f) => !f.name.startsWith("skills/")); + const configFiles = files.filter( + (f) => + isPromptMdPath(f.name) && + f.name !== "manifest.json" && + !f.name.startsWith("skills/") && + !isSubagentPath(f.name), + ); const skillFiles = files.filter((f) => f.name.startsWith("skills/")); + const subagentRaw = files.filter((f) => isSubagentPath(f.name)); const groups: Record = {}; for (const file of skillFiles) { const skillName = file.name.split("/")[1]; if (!skillName) continue; if (!groups[skillName]) { - groups[skillName] = { name: skillName, files: [] }; + groups[skillName] = { + name: skillName, + files: [], + emoji: "✨", + displayName: skillName, + description: "", + }; } groups[skillName].files.push(file); } - return { configFiles, skillGroups: Object.values(groups) }; + for (const group of Object.values(groups)) { + const skillMd = group.files.find((f) => f.name.endsWith("/SKILL.md")) + ?.content; + if (!skillMd) continue; + const preview = parseSkillPreviewFromMarkdown(skillMd, group.name); + group.emoji = preview.emoji; + group.displayName = preview.name; + group.description = preview.description; + } + + const subagentFiles = subagentRaw + .map((file) => { + const slug = file.name.slice("agents/".length, -".md".length); + const parsed = parseSubagentForm(file.content, slug); + return { + slug, + name: parsed.name || slug, + description: parsed.description, + emoji: parsed.emoji || "🤖", + file, + }; + }) + .sort((a, b) => a.slug.localeCompare(b.slug)); + + return { configFiles, skillGroups: Object.values(groups), subagentFiles }; } /** Workspace glob entry from GET /workspace/glob. */ diff --git a/dashboard/src/pages/Experts/components/rootDirTree.test.ts b/dashboard/src/pages/Experts/components/rootDirTree.test.ts index 8a96dc3e..e186d4ee 100644 --- a/dashboard/src/pages/Experts/components/rootDirTree.test.ts +++ b/dashboard/src/pages/Experts/components/rootDirTree.test.ts @@ -140,6 +140,36 @@ describe("rootDirTree helpers", () => { expect(pathExistsInTree(next, "/Users/jubaoliang")).toBe(true); }); + it("sanitizeTree removes duplicate values anywhere in the tree", () => { + const tree: DirTreeNode[] = [ + { + ...root, + children: [ + { + value: "/Users", + title: "Users", + isLeaf: false, + children: [ + { + value: "/Users/a", + title: "a", + isLeaf: false, + }, + { + value: "/Users/a", + title: "a-dup", + isLeaf: false, + }, + ], + }, + ], + }, + ]; + const next = sanitizeTree(tree); + expect(next[0].children?.[0].children).toHaveLength(1); + expect(next[0].children?.[0].children?.[0].value).toBe("/Users/a"); + }); + it("ancestorDirPaths returns parents from / down to the parent of path", () => { expect(ancestorDirPaths("/Users/jubaoliang/新建文件夹")).toEqual([ "/", diff --git a/dashboard/src/pages/Experts/components/rootDirTree.ts b/dashboard/src/pages/Experts/components/rootDirTree.ts index 5c2c7ca7..2fde44a1 100644 --- a/dashboard/src/pages/Experts/components/rootDirTree.ts +++ b/dashboard/src/pages/Experts/components/rootDirTree.ts @@ -38,7 +38,26 @@ export function ensurePathInTree( /** Keep a single `/` tree — root-level orphans duplicate keys and break expand. */ export function sanitizeTree(nodes: DirTreeNode[]): DirTreeNode[] { const root = nodes.find((node) => node.value === "/"); - return root ? [root] : nodes; + if (!root) return nodes; + + // Ant Design TreeSelect virtual scroll renders duplicate rows when the same + // value appears more than once anywhere in treeData (antd#37228). + const seen = new Set(); + + const walk = (node: DirTreeNode): DirTreeNode | null => { + if (seen.has(node.value)) return null; + seen.add(node.value); + const children = (node.children ?? []) + .map(walk) + .filter((child): child is DirTreeNode => child != null); + return { + ...node, + children: children.length > 0 ? children : undefined, + }; + }; + + const cleaned = walk(root); + return cleaned ? [cleaned] : [root]; } /** diff --git a/dashboard/src/pages/Experts/components/sharedExpert.test.ts b/dashboard/src/pages/Experts/components/sharedExpert.test.ts new file mode 100644 index 00000000..ff8ef425 --- /dev/null +++ b/dashboard/src/pages/Experts/components/sharedExpert.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { + isOwnedExpert, + isSharedExpertViewer, + ownedExperts, +} from "../../../utils/sharedExpert"; + +describe("isSharedExpertViewer", () => { + it("identifies a shared expert viewed by someone other than its owner", () => { + expect(isSharedExpertViewer({ is_shared: true, is_owner: false })).toBe( + true, + ); + }); + + it("does not treat the owner or a private expert as a viewer", () => { + expect(isSharedExpertViewer({ is_shared: true, is_owner: true })).toBe( + false, + ); + expect(isSharedExpertViewer({ is_shared: false, is_owner: false })).toBe( + false, + ); + }); +}); + +describe("ownedExperts", () => { + it("keeps owned experts and drops shared viewers for manage pages", () => { + const agents = [ + { agent_id: "own", is_shared: true, is_owner: true }, + { agent_id: "shared", is_shared: true, is_owner: false }, + { agent_id: "private", is_shared: false, is_owner: true }, + ]; + expect(ownedExperts(agents).map((a) => a.agent_id)).toEqual([ + "own", + "private", + ]); + expect(isOwnedExpert(agents[1]!)).toBe(false); + expect(isOwnedExpert(agents[0]!)).toBe(true); + }); +}); diff --git a/dashboard/src/pages/Experts/index.module.less b/dashboard/src/pages/Experts/index.module.less index 8cd4144f..3bd65e78 100644 --- a/dashboard/src/pages/Experts/index.module.less +++ b/dashboard/src/pages/Experts/index.module.less @@ -1359,6 +1359,7 @@ } .catalogCard { + position: relative; display: flex; flex-direction: column; gap: 10px; @@ -1399,6 +1400,15 @@ } } +.catalogCardAccent { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: var(--agent-accent, #6366f1); +} + .catalogCardHeader { display: flex; align-items: center; @@ -1794,3 +1804,27 @@ white-space: nowrap; flex-shrink: 0; } + +/* ── Publish expert drawer ─────────────────────────────────────── */ + +.publishDrawerBody { + display: flex; + flex-direction: column; + gap: 0; +} + +.publishDrawerLoading { + display: flex; + justify-content: center; + padding: 8px 0 12px; +} + +.publishDrawerHint { + margin-bottom: 16px; +} + +.publishDrawerFooter { + display: flex; + justify-content: flex-end; + gap: 8px; +} diff --git a/dashboard/src/pages/Experts/index.tsx b/dashboard/src/pages/Experts/index.tsx index 66a2ce72..ddd0a37e 100644 --- a/dashboard/src/pages/Experts/index.tsx +++ b/dashboard/src/pages/Experts/index.tsx @@ -25,17 +25,26 @@ import { message } from "@/utils/antdMessage"; import { LayoutGrid, List, RefreshCw } from "lucide-react"; import PageShell from "../../layouts/PageShell"; import { request } from "../../api/request"; +import { + publishedExpertsApi, + type PublishedExpert, +} from "../../api/modules/publishedExperts"; import { useAgent } from "../../context/AgentContext"; +import { useCurrentUser } from "../../hooks/useCurrentUser"; import { useCardTableView } from "../../hooks/useCardTableView"; import type { OctopAgent } from "../../context/AgentContext"; import { AgentCard } from "./components/AgentCard"; import { ExpertCard } from "./components/ExpertCard"; import type { ExpertSummary } from "./components/ExpertCard"; import EditAgentDrawer from "./components/EditAgentDrawer"; -import CreateFromExpertDrawer from "./components/CreateFromExpertDrawer"; +import CreateFromExpertDrawer, { + type CreateFromTemplateSource, +} from "./components/CreateFromExpertDrawer"; +import { PublishedExpertCard } from "./components/PublishedExpertCard"; import AgentExpertsTable from "./components/AgentExpertsTable"; import ExpertMarketTab from "./components/ExpertMarketTab"; import { OctopEmptyMascot } from "../../components/EmptyState"; +import { ownedExperts } from "../../utils/sharedExpert"; import styles from "./index.module.less"; type TabKey = "my" | "library" | "market"; @@ -51,6 +60,10 @@ async function fetchExpertLibrary(): Promise { return request("/experts"); } +async function fetchPublishedExperts(): Promise { + return publishedExpertsApi.list(); +} + async function fetchInstalledExpertIds(): Promise> { const data = await request<{ config?: { expert_id?: string } }[]>("/agents"); return new Set( @@ -62,6 +75,14 @@ export default function ExpertsPage() { const { t, i18n } = useTranslation(); const lang: "zh" | "en" = i18n.language?.startsWith("zh") ? "zh" : "en"; const { agents, refresh: refreshAgents } = useAgent(); + const currentUser = useCurrentUser(); + + const canManagePublished = useCallback( + (expert: PublishedExpert) => + currentUser?.role === "admin" || + String(currentUser?.id) === expert.created_by, + [currentUser], + ); // ── Tab state ────────────────────────────────────────────────── const [activeTab, setActiveTab] = useState("my"); @@ -73,13 +94,16 @@ export default function ExpertsPage() { const handleRefresh = useCallback(async () => { setRefreshing(true); try { - const [agentList, expertList, installedIds] = await Promise.all([ - request("/agents"), - fetchExpertLibrary(), - fetchInstalledExpertIds(), - ]); - setLocalAgents(agentList); + const [agentList, expertList, publishedList, installedIds] = + await Promise.all([ + request("/agents"), + fetchExpertLibrary(), + fetchPublishedExperts(), + fetchInstalledExpertIds(), + ]); + setLocalAgents(ownedExperts(agentList)); setExperts(expertList); + setPublishedExperts(publishedList); setAgentExpertIds(installedIds); await refreshAgents({ silent: true, force: true }); } catch (err: unknown) { @@ -100,13 +124,41 @@ export default function ExpertsPage() { // ── Built-in expert library ──────────────────────────────────── const [experts, setExperts] = useState([]); const [expertLoading, setExpertLoading] = useState(false); + const [publishedExperts, setPublishedExperts] = useState( + [], + ); + const [publishedExpertLoading, setPublishedExpertLoading] = useState(false); + + const publishedByAgentId = useMemo(() => { + const map: Record = {}; + for (const item of publishedExperts) { + if (item.source_agent_id) { + map[item.source_agent_id] = item; + } + } + return map; + }, [publishedExperts]); + + const refreshPublishedExperts = useCallback(async () => { + try { + setPublishedExperts(await fetchPublishedExperts()); + } catch (err: unknown) { + message.error( + err instanceof Error ? err.message : t("experts.loadFailed"), + ); + } + }, [t]); useEffect(() => { let cancelled = false; setExpertLoading(true); - fetchExpertLibrary() - .then((data) => { - if (!cancelled) setExperts(data); + setPublishedExpertLoading(true); + Promise.all([fetchExpertLibrary(), fetchPublishedExperts()]) + .then(([expertData, publishedData]) => { + if (!cancelled) { + setExperts(expertData); + setPublishedExperts(publishedData); + } }) .catch((err: unknown) => { if (cancelled) return; @@ -115,7 +167,10 @@ export default function ExpertsPage() { ); }) .finally(() => { - if (!cancelled) setExpertLoading(false); + if (!cancelled) { + setExpertLoading(false); + setPublishedExpertLoading(false); + } }); return () => { cancelled = true; @@ -123,11 +178,12 @@ export default function ExpertsPage() { }, [t]); // ── Local agent state (extends AgentContext for optimistic updates) ── - const [localAgents, setLocalAgents] = useState(agents); + const ownedAgents = useMemo(() => ownedExperts(agents), [agents]); + const [localAgents, setLocalAgents] = useState(ownedAgents); const [newAgentId, setNewAgentId] = useState(null); useEffect(() => { - setLocalAgents(agents); + setLocalAgents(ownedExperts(agents)); }, [agents]); const handleStateChange = useCallback((agentId: string, newState: string) => { @@ -148,7 +204,12 @@ export default function ExpertsPage() { ( updated: Pick< OctopAgent, - "agent_id" | "name" | "description" | "default_model" + | "agent_id" + | "name" + | "description" + | "default_model" + | "is_shared" + | "color" >, ) => { setEditAgent(null); @@ -160,6 +221,8 @@ export default function ExpertsPage() { name: updated.name, description: updated.description, default_model: updated.default_model, + is_shared: updated.is_shared, + color: updated.color, } : a, ), @@ -170,11 +233,12 @@ export default function ExpertsPage() { ); // ── Create-from-expert Drawer / Market create success ────────── - const [createExpert, setCreateExpert] = useState(null); + const [createSource, setCreateSource] = + useState(null); const handleCreated = useCallback( - (agentId: string) => { - setCreateExpert(null); + (agentId: string, _agentName?: string) => { + setCreateSource(null); void refreshAgents({ silent: true }); setActiveTab("my"); setNewAgentId(agentId); @@ -298,6 +362,10 @@ export default function ExpertsPage() { agent={agent} iconName={agent.icon_name} accentColor={agent.color} + publishedExpert={publishedByAgentId[agent.agent_id] ?? null} + onPublishedChange={() => { + void refreshPublishedExperts(); + }} onEdit={(id) => setEditAgent( localAgents.find((a) => a.agent_id === id) ?? null, @@ -312,6 +380,10 @@ export default function ExpertsPage() { ) : ( { + void refreshPublishedExperts(); + }} onEdit={(id) => setEditAgent(localAgents.find((a) => a.agent_id === id) ?? null) } @@ -326,20 +398,22 @@ export default function ExpertsPage() { localAgents, newAgentId, openExpertLibrary, + publishedByAgentId, refreshButton, + refreshPublishedExperts, showCardView, t, ]); const libraryContent = useMemo(() => { - if (expertLoading) { + if (expertLoading || publishedExpertLoading) { return (
); } - if (experts.length === 0) { + if (experts.length === 0 && publishedExperts.length === 0) { return (
@@ -353,12 +427,56 @@ export default function ExpertsPage() { } return ( <> + {publishedExperts.length > 0 && ( + <> +
+ + {t("experts.published.listTitle", { + count: publishedExperts.length, + })} + +
+

+ {t("experts.published.listHint")} +

+
+ {publishedExperts.map((expert) => ( + + setCreateSource({ kind: "published", expert: item }) + } + onChanged={refreshPublishedExperts} + /> + ))} +
+ + )}
{t("experts.totalLibrary", { count: experts.length })}
{refreshButton}
+ {publishedExperts.length === 0 && ( +

+ {t("experts.published.emptyHint")} +

+ )}
{experts.map((expert) => ( + setCreateSource({ kind: "builtin", expert: item }) + } /> ))}
); - }, [experts, expertLoading, lang, agentExpertIds, refreshButton, t]); + }, [ + agentExpertIds, + expertLoading, + experts, + lang, + publishedExpertLoading, + publishedExperts, + refreshButton, + t, + ]); const marketContent = useMemo( () => ( + setCreateSource({ kind: "market", expert }) + } /> ), - [agentExpertIds, handleCreated, lang], + [agentExpertIds, lang], ); return ( @@ -420,10 +551,10 @@ export default function ExpertsPage() { /> setCreateExpert(null)} + onClose={() => setCreateSource(null)} onCreated={handleCreated} /> diff --git a/dashboard/src/pages/KnowledgeBases/index.module.less b/dashboard/src/pages/KnowledgeBases/index.module.less new file mode 100644 index 00000000..70ff2b04 --- /dev/null +++ b/dashboard/src/pages/KnowledgeBases/index.module.less @@ -0,0 +1,959 @@ +.settingsBody { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 16px; +} + +.settingsFieldLabel { + color: var(--fn-text-secondary); + font-size: 13px; +} + +.settingsHint { + display: block; + margin-top: 4px; + font-size: 12px; + line-height: 1.5; +} + +.onnxModelList { + display: flex; + flex-direction: column; + gap: 8px; +} + +.onnxModelItem { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-md); + background: var(--fn-bg-primary); + color: inherit; + text-align: left; + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; + + &:hover { + border-color: color-mix( + in srgb, + var(--fn-color-brand) 40%, + var(--fn-border-secondary) + ); + } +} + +.onnxModelItemActive { + border-color: var(--fn-color-brand); + background: color-mix(in srgb, var(--fn-color-brand) 8%, transparent); + box-shadow: 0 0 0 1px + color-mix(in srgb, var(--fn-color-brand) 25%, transparent); +} + +.onnxModelInfo { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + flex: 1; +} + +.onnxModelName { + overflow: hidden; + color: var(--fn-text-primary); + font-size: 13px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.onnxModelMeta { + color: var(--fn-text-tertiary); + font-size: 12px; + line-height: 1.4; +} + +.showMoreOnnx { + height: 36px; + padding: 0; + color: var(--fn-text-secondary); +} + +.drawerFooter { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.downloadProgressLabel { + margin-bottom: 8px; + font-size: 13px; +} + +.helpIcon { + flex-shrink: 0; + color: var(--fn-text-tertiary); + cursor: help; + vertical-align: -2px; +} + +.modalChecks, +.memberList, +.actions, +.listActions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.switchLabel { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.baseModal :global(.ant-modal-body) { + padding-top: 12px; +} + +.baseForm :global(.ant-form-item) { + margin-bottom: 16px; +} + +.baseForm :global(.ant-form-item:last-child) { + margin-bottom: 0; +} + +.baseForm :global(.ant-form-item-label) { + padding-bottom: 6px !important; +} + +.baseForm :global(.ant-form-item-label > label) { + height: auto; + color: var(--fn-text-secondary); + font-size: 13px; +} + +.iconPicker { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.iconPickerItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + min-width: 0; + padding: 10px 6px; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-md); + background: var(--fn-bg-primary); + color: var(--fn-text-secondary); + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + color 0.15s ease, + box-shadow 0.15s ease; + + &:hover { + border-color: color-mix( + in srgb, + var(--fn-color-brand) 40%, + var(--fn-border-secondary) + ); + color: var(--fn-text-primary); + } +} + +.iconPickerItemActive { + border-color: var(--fn-color-brand); + background: color-mix(in srgb, var(--fn-color-brand) 8%, transparent); + color: var(--fn-color-brand); + box-shadow: 0 0 0 1px + color-mix(in srgb, var(--fn-color-brand) 25%, transparent); +} + +.iconPickerGlyph { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; +} + +.iconPickerLabel { + overflow: hidden; + max-width: 100%; + font-size: 12px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.formOptions { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 4px; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-md); + overflow: hidden; +} + +.formOptionRow { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + background: var(--fn-bg-primary); + + & + & { + border-top: 1px solid var(--fn-border-secondary); + } + + :global(.ant-switch) { + flex-shrink: 0; + margin-top: 2px; + } +} + +.formOptionCopy { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + flex: 1; +} + +.formOptionHint { + color: var(--fn-text-tertiary); + font-size: 12px; + line-height: 1.5; +} + +.layout { + display: flex; + height: 100%; + min-height: 0; + overflow: hidden; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-lg); + background: var(--fn-bg-container); +} + +.layoutResizing { + user-select: none; + cursor: col-resize; +} + +.baseList { + display: flex; + flex-direction: column; + gap: 12px; + flex-shrink: 0; + width: var(--knowledge-bases-sidebar-width, 280px); + min-height: 0; + padding: 16px; + background: var(--fn-bg-secondary); +} + +.listPanelHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-shrink: 0; +} + +.listPanelTitle { + min-width: 0; + overflow: hidden; + color: var(--fn-text-primary); + font-size: 13px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.listPanelToggle, +.listPanelExpandBtn { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + border: none; + border-radius: 8px; + background: transparent; + color: var(--fn-text-tertiary); + cursor: pointer; + transition: + background 0.15s ease, + color 0.15s ease; + + &:hover { + background: var(--fn-bg-hover); + color: var(--fn-text-primary); + } +} + +.listPanelToggle { + width: 28px; + height: 28px; +} + +.listPanelExpandBtn { + position: absolute; + top: 16px; + left: 12px; + z-index: 1; + width: 32px; + height: 32px; + border: 1px solid var(--fn-border-secondary); + background: var(--fn-bg-container); + box-shadow: var(--fn-shadow-sm); +} + +.listActions > :first-child { + flex: 1; +} + +.list { + min-height: 0; + overflow-y: auto; + padding: 4px 2px; +} + +.list :global(.ant-list-items) { + padding-bottom: 8px; +} + +.list :global(.ant-list-item) { + border-bottom: none !important; +} + +.listRow { + padding: 0 !important; + margin: 0 0 12px !important; +} + +.listRow:last-child { + margin-bottom: 0 !important; +} + +.listItem { + display: flex !important; + flex-direction: column; + width: 100%; + box-sizing: border-box; + min-height: 116px; + padding: 12px !important; + border: 1px solid var(--fn-border-secondary) !important; + border-radius: var(--fn-radius-md); + background: var(--fn-bg-primary); + cursor: pointer; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease, + transform 0.15s ease, + background 0.15s ease; + + &:hover { + border-color: color-mix( + in srgb, + var(--fn-color-brand) 45%, + var(--fn-border-secondary) + ) !important; + box-shadow: var(--fn-shadow-sm); + transform: translateY(-1px); + } +} + +.active { + border-color: color-mix( + in srgb, + var(--fn-color-brand) 65%, + var(--fn-border-secondary) + ) !important; + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--fn-color-brand) 30%, transparent), + 0 8px 20px color-mix(in srgb, var(--fn-color-brand) 10%, transparent); +} + +.listName { + display: flex; + align-items: center; + gap: 10px; + overflow: hidden; + color: var(--fn-text-primary); + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.listIcon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + overflow: hidden; + border-radius: 8px; + color: var(--fn-color-brand); + background: color-mix(in srgb, var(--fn-color-brand) 10%, transparent); +} + +.listDescription { + display: -webkit-box; + min-height: 34px; + margin: 6px 0 10px; + overflow: hidden; + color: var(--fn-text-tertiary); + font-size: 12px; + line-height: 1.45; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.listMeta { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + gap: 8px; + margin-top: auto; +} + +.listCountTag { + margin-inline-end: 0 !important; + border-color: var(--fn-border-secondary); + background: var(--fn-bg-secondary); + color: var(--fn-text-tertiary); +} + +.listMetaBadges { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 2px; +} + +.listBadge { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 6px; + border-radius: 4px; + font-size: 12px; + line-height: 20px; + white-space: nowrap; +} + +.listBadgeDefaultOpen { + color: #1677ff; + background: #e6f4ff; +} + +.listBadgeShared { + color: #08979c; + background: #e6fffb; +} + +.splitDivider { + position: relative; + flex-shrink: 0; + align-self: stretch; + width: 8px; + margin: 0 -1px; + z-index: 2; + background: var(--fn-border-secondary); +} + +.resizeHandle { + position: absolute; + inset: 0; + cursor: col-resize; + z-index: 1; + + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 36px; + border-radius: 2px; + background: var(--fn-border-secondary); + opacity: 0.55; + transition: + opacity 0.15s, + background 0.15s; + } + + &:hover::after, + .layoutResizing &::after { + opacity: 1; + background: var(--fn-text-tertiary); + } +} + +.detail { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + overflow: hidden; + padding: 16px 20px 0; +} + +.detailListCollapsed { + padding-left: 52px; +} + +.detailLoading, +.centered { + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; +} + +.detailLoading { + position: absolute; + inset: 0; + z-index: 3; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 48px; + background: color-mix(in srgb, var(--fn-bg-container) 72%, transparent); + pointer-events: none; +} + +.detailHeader { + display: flex; + flex-direction: column; + gap: 4px; + flex-shrink: 0; + padding-bottom: 12px; + border-bottom: 1px solid var(--fn-border-secondary); +} + +.titleRow, +.titleGroup, +.sectionHeader { + display: flex; + align-items: center; + gap: 16px; +} + +.titleGroup { + justify-content: flex-start; + min-width: 0; + flex: 1; + gap: 4px; + flex-wrap: wrap; +} + +.detailTitle { + margin: 0 !important; + min-width: 0; + flex: 0 1 auto; +} + +.titleActions { + display: inline-flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + +.titleActionBtn { + width: 28px; + height: 28px; + padding: 0; + color: var(--fn-text-tertiary); + + &:hover { + color: var(--fn-text-secondary) !important; + } +} + +.detailDescription { + margin: 0 !important; + max-width: 100%; +} + +.detailMeta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 16px; + margin-top: 4px; + max-width: 100%; +} + +.detailCreator { + margin: 0 !important; + max-width: 100%; + font-size: 12px; + line-height: 1.5715; +} + +.detailBody { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + overflow: auto; +} + +.docsToolbar { + margin: 12px 0 8px; +} + +.uploadHint { + display: block; + margin: 0 0 12px; + font-size: 12px; +} + +.docsCount { + font-size: 13px; + color: var(--fn-text-tertiary); +} + +.viewModeLabel { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.emptyDetail { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 20px; + height: 100%; + min-height: 180px; + padding: 80px 24px 24px; + text-align: center; +} + +.emptyDetailText { + margin: 0; + max-width: 320px; + font-size: 14px; + line-height: 1.6; + color: var(--fn-text-tertiary); +} + +.emptyLayout { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: auto; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-lg); + background: var(--fn-bg-container); +} + +.emptyLayoutMobile { + border: none; + border-radius: 0; + background: transparent; +} + +.emptyGuide { + flex: 1; +} + +.setupMascot { + display: block; + width: 120px; + height: 120px; + object-fit: contain; + user-select: none; + -webkit-user-drag: none; +} + +.docCardGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 14px; +} + +.docCard { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + min-height: 0; + padding: 14px; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-md); + background: var(--fn-bg-primary); + transition: + box-shadow 0.2s ease, + border-color 0.2s ease; + + &:hover { + border-color: color-mix( + in srgb, + var(--fn-color-brand) 45%, + var(--fn-border-secondary) + ); + box-shadow: var(--fn-shadow-sm); + } +} + +.docCardHeader { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; +} + +.docFormatIcon, +.docTableIcon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; +} + +.docFormatIcon { + width: 28px; + height: 28px; + margin-top: 1px; + border-radius: 7px; +} + +.docTableIcon { + width: 22px; + height: 22px; + border-radius: 6px; +} + +.docCardTitleBlock { + flex: 1; + min-width: 0; +} + +.docCardTitleRow { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.docCardName { + overflow: hidden; + color: var(--fn-text-primary); + font-weight: 600; + font-size: 13px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.docExtBadge { + flex-shrink: 0; + padding: 0 5px; + border-radius: 4px; + background: var(--fn-bg-secondary); + color: var(--fn-text-tertiary); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.02em; + line-height: 16px; +} + +.docCardMeta { + margin-top: 3px; + color: var(--fn-text-tertiary); + font-size: 12px; + line-height: 1.35; +} + +.docCardActions { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 0; +} + +.docCardFooter { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.docStatusTag { + margin-inline-end: 0 !important; + padding-inline: 6px; + font-size: 12px; + line-height: 18px; +} + +.docUpdatedAt { + flex: 1; + min-width: 0; + overflow: hidden; + color: var(--fn-text-tertiary); + font-size: 12px; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tableFilename { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; + max-width: 100%; + + > span:nth-child(2) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.tableActions { + display: inline-flex; + align-items: center; + gap: 0; +} + +.iconPreview { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + color: var(--fn-color-brand); +} + +.previewBody { + max-height: min(60vh, 560px); + margin: 0; + overflow: auto; + padding: 12px 14px; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-md); + background: var(--fn-bg-secondary); + color: var(--fn-text-primary); + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + +.memberList { + margin-top: 8px; +} + +.fileInput { + display: none; +} + +.limitAlert { + margin: 0 0 12px; +} + +.featureBackend { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + margin-bottom: 12px; +} + +.featureFields { + display: flex; + flex-direction: column; + gap: 12px; +} + +.mobileBack { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 32px; + height: 32px; + margin-top: 2px; + margin-right: 2px; + padding: 0; + border: 0; + border-radius: 8px; + color: var(--fn-text-secondary); + background: transparent; + cursor: pointer; + + &:hover { + background: var(--fn-bg-hover); + color: var(--fn-text-primary); + } +} + +@media (max-width: 767px) { + .layoutMobile { + border: 0; + border-radius: 0; + background: transparent; + } + + .layoutMobile .baseList { + width: 100% !important; + flex: 1; + min-height: 0; + padding: 0; + background: transparent; + } + + .layoutMobile .listItem { + min-height: 104px; + } + + .layoutMobile .detail { + flex: 1; + min-height: 0; + padding: 0; + border: 1px solid var(--fn-border-secondary); + border-radius: var(--fn-radius-lg); + background: var(--fn-bg-container); + } + + .layoutMobile .detailHeader { + padding: 0 4px 12px; + } + + .layoutMobile .titleRow { + flex-wrap: wrap; + gap: 10px; + } + + .layoutMobile .detailBody { + padding: 0 4px; + } +} diff --git a/dashboard/src/pages/KnowledgeBases/index.tsx b/dashboard/src/pages/KnowledgeBases/index.tsx new file mode 100644 index 00000000..5baab2e7 --- /dev/null +++ b/dashboard/src/pages/KnowledgeBases/index.tsx @@ -0,0 +1,1727 @@ +import { + useCallback, + useEffect, + useRef, + useState, + type CSSProperties, +} from "react"; +import { + Alert, + Button, + Drawer, + Empty, + Form, + Input, + List, + Modal, + Popconfirm, + Progress, + Radio, + Segmented, + Select, + Spin, + Switch, + Table, + Tag, + Tooltip, + Typography, +} from "antd"; +import { message } from "@/utils/antdMessage"; +import { + ChevronLeft, + Download, + Eye, + FileUp, + LayoutGrid, + List as ListIcon, + PanelLeftClose, + PanelLeftOpen, + Pencil, + Plus, + RefreshCw, + Settings, + Trash2, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; + +import { useCurrentUser } from "../../hooks/useCurrentUser"; +import { userCan } from "../../utils/permissions"; +import { + DEFAULT_KNOWLEDGE_LIMITS, + knowledgeBasesApi, + type KnowledgeBase, + type KnowledgeCapability, + type KnowledgeDocument, + type KnowledgeOnnxModel, +} from "../../api/modules/knowledgeBases"; +import { OctopEmptyMascot } from "../../components/EmptyState"; +import StreamSetupGuide from "../../components/StreamSetupGuide/StreamSetupGuide"; +import { CopyableResourceId } from "../../components/CopyableResourceId"; +import { useCardTableView } from "../../hooks/useCardTableView"; +import { useHorizontalResize } from "../../hooks/useHorizontalResize"; +import { useIsMobile } from "../../hooks/useIsMobile"; +import { useListPanelCollapsed } from "../../hooks/useListPanelCollapsed"; +import { useServerTimezone } from "../../hooks/useServerTimezone"; +import PageShell from "../../layouts/PageShell"; +import { apiErrorMessage, isNotFoundApiError } from "../../utils/apiError"; +import { createDetailRequestGate } from "../../utils/detailRequestGate"; +import { formatBytes, formatSizeGb } from "../../utils/embeddingDownload"; +import { fileTreeIconSpec } from "../../utils/fileTreeIcon"; +import { formatServerDateTime } from "../../utils/formatMessageTime"; +import skillStyles from "../Agent/Skills/index.module.less"; +import { KNOWLEDGE_ICON_NAMES, knowledgeIconForName } from "./knowledgeIcons"; +import styles from "./index.module.less"; + +type BaseFormValues = { + name: string; + description?: string; + icon_name?: string; +}; + +type DocsViewMode = "card" | "table"; +const DOCS_VIEW_STORAGE_KEY = "octop:knowledge-bases-docs-view"; + +const SUPPORTED_DOCUMENT_TYPES = ".md,.txt,.pdf,.docx,.pptx"; + +function loadDocsViewMode(): DocsViewMode { + const stored = localStorage.getItem(DOCS_VIEW_STORAGE_KEY); + return stored === "table" ? "table" : "card"; +} + +function documentStatusColor(status: KnowledgeDocument["status"]) { + if (status === "ready") return "success"; + if (status === "failed") return "error"; + if (status === "processing") return "processing"; + return "default"; +} + +function fileExtensionLabel(filename: string): string { + const ext = filename.includes(".") + ? filename.slice(filename.lastIndexOf(".") + 1) + : ""; + return ext.trim().toUpperCase().slice(0, 5); +} + +function formatKnowledgeOwner( + base: Pick< + KnowledgeBase, + "owner_display_name" | "owner_username" | "owner_user_id" + >, +): string { + const displayName = base.owner_display_name?.trim() || ""; + const username = base.owner_username?.trim() || ""; + return displayName || username || String(base.owner_user_id); +} + +function DocumentFormatIcon({ + filename, + size = 14, + className, +}: { + filename: string; + size?: number; + className?: string; +}) { + const { Icon, color } = fileTreeIconSpec(filename); + return ( + + + + ); +} + +function KnowledgeIconPicker({ + value, + onChange, +}: { + value?: string; + onChange?: (value?: string) => void; +}) { + const { t } = useTranslation(); + return ( +
+ {KNOWLEDGE_ICON_NAMES.map((name) => { + const selected = value === name; + return ( + + ); + })} +
+ ); +} + +export default function KnowledgeBasesPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const isMobile = useIsMobile(); + const timeZone = useServerTimezone(); + const user = useCurrentUser(); + const canConfigureKb = userCan(user, "knowledge_settings"); + const { viewMode, setViewMode, showCardView } = useCardTableView( + loadDocsViewMode(), + ); + const [bases, setBases] = useState([]); + const [selected, setSelected] = useState(null); + const [documents, setDocuments] = useState([]); + const [capability, setCapability] = useState( + null, + ); + const [catalog, setCatalog] = useState([]); + const [onnxDownloading, setOnnxDownloading] = useState(false); + const [downloadProgressOpen, setDownloadProgressOpen] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); + const [downloadProgressLabel, setDownloadProgressLabel] = useState(""); + const [downloadProgressModel, setDownloadProgressModel] = useState(""); + const onnxDownloadTimer = useRef | null>(null); + const [remoteProviders, setRemoteProviders] = useState< + { + provider_id: string; + provider_name: string; + models: { id: string; name: string }[]; + }[] + >([]); + const [loading, setLoading] = useState(true); + const [detailLoading, setDetailLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [mobilePane, setMobilePane] = useState<"list" | "detail">("list"); + const [baseModalOpen, setBaseModalOpen] = useState(false); + const [editingBase, setEditingBase] = useState(false); + const [featureModalOpen, setFeatureModalOpen] = useState(false); + const [featureEnabledDraft, setFeatureEnabledDraft] = useState(false); + const [featureModel, setFeatureModel] = useState(); + const [featureBackend, setFeatureBackend] = useState<"onnx" | "remote">( + "onnx", + ); + const [featureProviderId, setFeatureProviderId] = useState(); + const [featureOptionsLoading, setFeatureOptionsLoading] = useState(false); + const [onnxExpanded, setOnnxExpanded] = useState(false); + const [onnxExpanding, setOnnxExpanding] = useState(false); + const onnxExpandedRef = useRef(false); + const [previewOpen, setPreviewOpen] = useState(false); + const [previewLoading, setPreviewLoading] = useState(false); + const [previewFilename, setPreviewFilename] = useState(""); + const [previewText, setPreviewText] = useState(""); + const [baseForm] = Form.useForm(); + const [defaultOpenChecked, setDefaultOpenChecked] = useState(false); + const [sharedChecked, setSharedChecked] = useState(false); + const uploadRef = useRef(null); + const detailRequestGate = useRef(createDetailRequestGate()); + const { + size: sidebarWidth, + isResizing, + onResizeStart, + } = useHorizontalResize({ + min: 220, + max: 480, + defaultSize: 280, + storageKey: "octop:knowledge-bases:sidebar-width", + }); + const { collapsed: listPanelCollapsed, toggle: toggleListPanel } = + useListPanelCollapsed("octop:knowledge-bases:list-collapsed"); + + const canManageSelected = Boolean( + selected && + user && + (user.role === "admin" || selected.owner_user_id === user.id), + ); + const canWriteSelected = canManageSelected; + const usable = Boolean(capability?.usable); + const limits = capability?.limits ?? DEFAULT_KNOWLEDGE_LIMITS; + const ownedBaseCount = user + ? bases.filter((base) => base.owner_user_id === user.id).length + : 0; + const atBaseLimit = ownedBaseCount >= limits.max_bases_per_owner; + const isAtDocumentLimit = documents.length >= limits.max_docs_per_kb; + + const loadBases = useCallback(async () => { + try { + const rows = await knowledgeBasesApi.list(); + setBases(rows); + setSelected((current) => + current && !rows.some((row) => row.id === current.id) ? null : current, + ); + } catch (error) { + message.error(apiErrorMessage(error, t("knowledgeBases.loadFailed"), t)); + } + }, [t]); + + const loadCapability = useCallback(async () => { + try { + const nextCapability = await knowledgeBasesApi.getCapability(); + setCapability(nextCapability); + setFeatureModel(nextCapability.selected_model || undefined); + setFeatureBackend(nextCapability.backend); + setFeatureProviderId(nextCapability.provider_id || undefined); + setFeatureEnabledDraft(Boolean(nextCapability.feature_enabled)); + } catch (error) { + message.error(apiErrorMessage(error, t("knowledgeBases.loadFailed"), t)); + } + }, [t]); + + const loadEmbeddingOptions = useCallback( + async (allOnnx = false) => { + if (allOnnx) { + setOnnxExpanding(true); + } else { + setFeatureOptionsLoading(true); + } + try { + const options = await knowledgeBasesApi.getEmbeddingOptions({ + allOnnx, + }); + setCatalog(options.onnx); + setRemoteProviders(options.remote); + if (allOnnx) { + onnxExpandedRef.current = true; + setOnnxExpanded(true); + } + return options; + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.loadFailed"), t), + ); + return undefined; + } finally { + setFeatureOptionsLoading(false); + setOnnxExpanding(false); + } + }, + [t], + ); + + const stopOnnxDownloadWatch = useCallback(() => { + if (onnxDownloadTimer.current) { + clearInterval(onnxDownloadTimer.current); + onnxDownloadTimer.current = null; + } + }, []); + + const applyOnnxDownloadProgress = useCallback( + (modelId: string, status: string, progress: number) => { + const pct = Math.max(0, Math.min(100, Math.round((progress || 0) * 100))); + setDownloadProgress(pct); + if (status === "loading") { + setDownloadProgressLabel( + t("models.onnxDownloadLoading", { model: modelId }), + ); + } else if (status === "downloading") { + setDownloadProgressLabel( + t("models.onnxDownloadProgress", { model: modelId, percent: pct }), + ); + } else { + setDownloadProgressLabel(t("models.localDownloadPreparing")); + } + }, + [t], + ); + + const finishOnnxDownload = useCallback( + async (modelId: string, status: string, error?: string | null) => { + stopOnnxDownloadWatch(); + setOnnxDownloading(false); + setDownloadProgressOpen(false); + await loadEmbeddingOptions(onnxExpandedRef.current); + if (status === "done") { + try { + await knowledgeBasesApi.activateOnnx(modelId); + message.success(t("knowledgeBases.onnxServiceEnabled")); + } catch (activateError) { + message.success(t("models.onnxDownloadDone", { model: modelId })); + message.warning( + apiErrorMessage( + activateError, + t("knowledgeBases.featureSaveFailed"), + t, + ), + ); + } + return; + } + message.error(error || t("models.onnxDownloadFailed")); + }, + [loadEmbeddingOptions, stopOnnxDownloadWatch, t], + ); + + const watchOnnxDownloadStatus = useCallback( + (modelId: string) => { + stopOnnxDownloadWatch(); + let inFlight = false; + let stopped = false; + const tick = async () => { + if (inFlight || stopped) return; + inFlight = true; + try { + const state = await knowledgeBasesApi.getOnnxDownloadStatus(); + applyOnnxDownloadProgress( + state.model_name || modelId, + state.status, + state.progress, + ); + if (state.status === "done" || state.status === "failed") { + stopped = true; + stopOnnxDownloadWatch(); + await finishOnnxDownload( + state.model_name || modelId, + state.status, + state.error, + ); + } + } catch (error) { + stopped = true; + stopOnnxDownloadWatch(); + await finishOnnxDownload( + modelId, + "failed", + error instanceof Error ? error.message : String(error), + ); + } finally { + inFlight = false; + } + }; + onnxDownloadTimer.current = setInterval(() => { + void tick(); + }, 500); + void tick(); + }, + [applyOnnxDownloadProgress, finishOnnxDownload, stopOnnxDownloadWatch], + ); + + useEffect(() => () => stopOnnxDownloadWatch(), [stopOnnxDownloadWatch]); + + const loadDetail = useCallback( + async (id: string, options?: { silent?: boolean }) => { + const requestId = detailRequestGate.current.begin(); + if (!options?.silent) setDetailLoading(true); + try { + const [base, nextDocuments] = await Promise.all([ + knowledgeBasesApi.get(id), + knowledgeBasesApi.listDocuments(id), + ]); + if (!detailRequestGate.current.isCurrent(requestId)) return; + setSelected(base); + setDocuments(nextDocuments); + } catch (error) { + if (!detailRequestGate.current.isCurrent(requestId)) return; + if (isNotFoundApiError(error)) { + setSelected(null); + setDocuments([]); + return; + } + if (!options?.silent) { + message.error( + apiErrorMessage(error, t("knowledgeBases.loadFailed"), t), + ); + } + } finally { + if ( + !options?.silent && + detailRequestGate.current.isCurrent(requestId) + ) { + setDetailLoading(false); + } + } + }, + [t], + ); + + useEffect(() => { + void Promise.all([loadBases(), loadCapability()]).finally(() => + setLoading(false), + ); + }, [loadBases, loadCapability]); + + useEffect(() => { + const indexing = documents.some( + (document) => + document.status === "pending" || document.status === "processing", + ); + if (!selected || !indexing) return; + const timer = window.setInterval(() => { + void loadDetail(selected.id, { silent: true }); + }, 2500); + return () => window.clearInterval(timer); + }, [documents, loadDetail, selected]); + useEffect(() => { + if (!isMobile && !selected && bases.length > 0 && !detailLoading) { + void loadDetail(bases[0].id); + } + }, [bases, detailLoading, isMobile, loadDetail, selected]); + + useEffect(() => { + if (!isMobile) setMobilePane("list"); + }, [isMobile]); + + const refresh = async () => { + setRefreshing(true); + try { + await Promise.all([ + loadBases(), + loadCapability(), + selected ? loadDetail(selected.id) : Promise.resolve(), + ]); + } finally { + setRefreshing(false); + } + }; + + const selectBase = (base: KnowledgeBase) => { + if (base.id !== selected?.id) void loadDetail(base.id); + if (isMobile) setMobilePane("detail"); + }; + + const openCreate = () => { + if (atBaseLimit) { + message.warning( + t("knowledgeBases.baseLimitReached", { + count: limits.max_bases_per_owner, + }), + ); + return; + } + baseForm.setFieldsValue({ + name: "", + description: "", + icon_name: "book-open", + }); + setDefaultOpenChecked(false); + setSharedChecked(false); + setEditingBase(false); + setBaseModalOpen(true); + }; + + const openEdit = () => { + if (!selected) return; + baseForm.setFieldsValue({ + name: selected.name, + description: selected.description, + icon_name: selected.icon_name || undefined, + }); + setDefaultOpenChecked(selected.default_open); + setSharedChecked(selected.shared); + setEditingBase(true); + setBaseModalOpen(true); + }; + + const saveBase = async () => { + const values = await baseForm.validateFields(); + const payload = { + ...values, + default_open: defaultOpenChecked, + shared: sharedChecked, + }; + try { + const next = + editingBase && selected + ? await knowledgeBasesApi.update(selected.id, payload) + : await knowledgeBasesApi.create(payload); + setBaseModalOpen(false); + await loadBases(); + await loadDetail(next.id); + if (isMobile) setMobilePane("detail"); + message.success( + t(editingBase ? "knowledgeBases.updated" : "knowledgeBases.created"), + ); + } catch (error) { + message.error(apiErrorMessage(error, t("knowledgeBases.saveFailed"), t)); + } + }; + + const deleteBase = async () => { + if (!selected) return; + const deletedId = selected.id; + detailRequestGate.current.begin(); + setSelected(null); + setDocuments([]); + setDetailLoading(false); + try { + await knowledgeBasesApi.delete(deletedId); + const rows = await knowledgeBasesApi.list(); + setBases(rows); + if (isMobile) { + setMobilePane("list"); + } else if (rows.length > 0) { + await loadDetail(rows[0].id); + } + message.success(t("knowledgeBases.deleted")); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.deleteFailed"), t), + ); + } + }; + + const saveFeature = async (confirmed = false) => { + if (!featureEnabledDraft) { + try { + setCapability(await knowledgeBasesApi.setFeature({ enabled: false })); + setFeatureModalOpen(false); + message.success(t("knowledgeBases.featureDisabled")); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.featureSaveFailed"), t), + ); + } + return; + } + if (!featureModel || (featureBackend === "remote" && !featureProviderId)) { + return; + } + if ( + featureBackend === "onnx" && + !catalog.find((model) => model.id === featureModel)?.downloaded + ) { + message.warning(t("knowledgeBases.downloadNeedModel")); + return; + } + if ( + !confirmed && + capability?.feature_enabled && + (capability.backend !== featureBackend || + capability.selected_model !== featureModel || + capability.provider_id !== + (featureBackend === "remote" ? featureProviderId : "")) + ) { + Modal.confirm({ + title: t("knowledgeBases.rebuildConfirmTitle"), + content: t("knowledgeBases.rebuildConfirmDescription"), + onOk: () => void saveFeature(true), + }); + return; + } + try { + const next = await knowledgeBasesApi.setFeature({ + enabled: true, + backend: featureBackend, + model: featureModel, + provider_id: featureBackend === "remote" ? featureProviderId : "", + }); + setCapability(next); + setFeatureModalOpen(false); + message.success(t("knowledgeBases.featureEnabled")); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.featureSaveFailed"), t), + ); + } + }; + + const openSettings = () => { + const selectedModel = capability?.selected_model || undefined; + const selectedBackend = capability?.backend ?? "onnx"; + onnxExpandedRef.current = false; + setOnnxExpanded(false); + setFeatureEnabledDraft(Boolean(capability?.feature_enabled)); + setFeatureBackend(selectedBackend); + setFeatureModel(selectedModel); + setFeatureProviderId(capability?.provider_id || undefined); + setFeatureModalOpen(true); + void (async () => { + const options = await loadEmbeddingOptions(false); + const selectedInCatalog = Boolean( + selectedModel && + options?.onnx.some((model) => model.id === selectedModel), + ); + if (selectedBackend === "onnx" && selectedModel && !selectedInCatalog) { + await loadEmbeddingOptions(true); + } + })(); + void knowledgeBasesApi.getOnnxDownloadStatus().then((state) => { + if (state.status === "downloading" || state.status === "loading") { + const modelId = state.model_name; + setOnnxDownloading(true); + setDownloadProgressModel(modelId); + setDownloadProgressOpen(true); + applyOnnxDownloadProgress(modelId, state.status, state.progress); + watchOnnxDownloadStatus(modelId); + } + }); + }; + + const startOnnxDownload = async (modelId: string) => { + const selected = catalog.find((model) => model.id === modelId); + const size = formatSizeGb(selected?.size_gb); + Modal.confirm({ + title: t("models.localDownloadConfirmTitle"), + content: t("models.localDownloadConfirmOnnx", { + name: modelId, + size: size || t("knowledgeBases.sizeUnknown"), + }), + okText: t("knowledgeBases.downloadModel"), + cancelText: t("common.cancel"), + onOk: async () => { + setOnnxDownloading(true); + setDownloadProgressModel(modelId); + setDownloadProgress(0); + setDownloadProgressLabel(t("models.localDownloadPreparing")); + setDownloadProgressOpen(true); + try { + await knowledgeBasesApi.downloadOnnx(modelId); + watchOnnxDownloadStatus(modelId); + } catch (error) { + setOnnxDownloading(false); + setDownloadProgressOpen(false); + message.error( + apiErrorMessage(error, t("models.onnxDownloadFailed"), t), + ); + } + }, + }); + }; + + const dismissDownloadProgressToBackground = () => { + setDownloadProgressOpen(false); + message.info(t("models.localDownloadBackground")); + }; + + const uploadDocuments = async (files: FileList | null) => { + if (!selected || !files || !usable || isAtDocumentLimit) return; + const remaining = Math.max(0, limits.max_docs_per_kb - documents.length); + const chosen = Array.from(files).slice(0, remaining); + const oversized = chosen.filter( + (file) => file.size > limits.max_document_bytes, + ); + if (oversized.length > 0) { + message.error( + t("knowledgeBases.documentTooLarge", { + sizeMb: Math.round(limits.max_document_bytes / (1024 * 1024)), + }), + ); + if (uploadRef.current) uploadRef.current.value = ""; + return; + } + try { + for (const file of chosen) { + await knowledgeBasesApi.uploadDocument(selected.id, file); + } + await loadDetail(selected.id); + await loadBases(); + message.success(t("knowledgeBases.uploaded")); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.uploadFailed"), t), + ); + } finally { + if (uploadRef.current) uploadRef.current.value = ""; + } + }; + + const deleteDocument = async (documentId: string) => { + if (!selected) return; + try { + await knowledgeBasesApi.deleteDocument(selected.id, documentId); + await loadDetail(selected.id); + await loadBases(); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.deleteFailed"), t), + ); + } + }; + + const rebuildDocument = async (documentId: string) => { + if (!selected) return; + try { + await knowledgeBasesApi.reindexDocument(selected.id, documentId); + message.success(t("knowledgeBases.rebuildDocumentSuccess")); + await loadDetail(selected.id); + } catch (error) { + message.error( + apiErrorMessage(error, t("knowledgeBases.rebuildDocumentFailed"), t), + ); + } + }; + + const openDocumentPreview = async (documentId: string) => { + if (!selected) return; + setPreviewOpen(true); + setPreviewLoading(true); + setPreviewFilename(""); + setPreviewText(""); + try { + const preview = await knowledgeBasesApi.previewDocument( + selected.id, + documentId, + ); + setPreviewFilename(preview.filename); + setPreviewText( + preview.text.trim() ? preview.text : t("knowledgeBases.previewEmpty"), + ); + } catch (error) { + setPreviewOpen(false); + message.error( + apiErrorMessage(error, t("knowledgeBases.previewFailed"), t), + ); + } finally { + setPreviewLoading(false); + } + }; + + const renderDocumentActions = (document: KnowledgeDocument) => ( +
+ +
+ ); + + const showListPane = !isMobile || mobilePane === "list"; + const showDetailPane = !isMobile || mobilePane === "detail"; + const showListPanel = showListPane && (isMobile || !listPanelCollapsed); + const showEnableGuide = !loading && !usable; + const showEmptyGuide = !loading && usable && bases.length === 0; + const emptyLayoutClassName = `${styles.emptyLayout}${ + isMobile ? ` ${styles.emptyLayoutMobile}` : "" + }`; + const setupMascot = ( + + ); + + const onDocsViewChange = (value: string | number) => { + const mode = value === "table" ? "table" : "card"; + setViewMode(mode); + localStorage.setItem(DOCS_VIEW_STORAGE_KEY, mode); + }; + + return ( + } onClick={openSettings}> + {t("knowledgeBases.settingsTitle")} + + ) : undefined + } + > + {loading ? ( +
+
+ +
+
+ ) : showEnableGuide ? ( +
+ , + } + : undefined + } + /> +
+ ) : showEmptyGuide ? ( +
+ , + disabled: atBaseLimit, + }} + /> +
+ ) : ( +
+ {showListPanel ? ( + + ) : null} + {!isMobile && !listPanelCollapsed ? ( +
+
+
+ ) : null} + {showDetailPane ? ( +
+ {!isMobile && listPanelCollapsed ? ( + + + + ) : null} + {detailLoading ? ( +
+ +
+ ) : null} + {!selected && !detailLoading ? ( +
+ +

+ {t("knowledgeBases.selectBase")} +

+
+ ) : !selected ? null : ( + <> +
+
+
+ {isMobile ? ( + + ) : null} + + {selected.name} + + {canManageSelected ? ( +
+ +
+ ) : null} +
+
+ + {selected.description || + t("knowledgeBases.noDescription")} + +
+ + + {t("knowledgeBases.createdBy", { + name: formatKnowledgeOwner(selected), + })} + +
+
+ +
+
+ + {t("knowledgeBases.documentLimit", { + count: documents.length, + max: limits.max_docs_per_kb, + })} + +
+ + + {t("knowledgeBases.viewCard")} + + ), + }, + { + value: "table", + label: ( + + + {t("knowledgeBases.viewTable")} + + ), + }, + ]} + /> + + void uploadDocuments(event.target.files) + } + /> + {canWriteSelected ? ( + + ) : null} +
+
+ {canWriteSelected ? ( + + {t("knowledgeBases.uploadHint", { + sizeMb: Math.round( + limits.max_document_bytes / (1024 * 1024), + ), + })} + + ) : null} + {isAtDocumentLimit ? ( + + ) : null} + {documents.length === 0 ? ( + + ) : showCardView ? ( +
+ {documents.map((document) => ( +
+
+ +
+
+
+ {document.filename} +
+ {fileExtensionLabel(document.filename) ? ( + + {fileExtensionLabel(document.filename)} + + ) : null} +
+
+ {formatBytes(document.byte_size)} + {" · "} + {t("knowledgeBases.chunkCount", { + count: document.chunk_count, + })} +
+
+ {renderDocumentActions(document)} +
+
+ + + {t( + `knowledgeBases.statusesShort.${document.status}`, + )} + + + + {formatServerDateTime( + document.updated_at, + timeZone, + )} + +
+
+ ))} +
+ ) : ( + ( + + + {filename} + {fileExtensionLabel(filename) ? ( + + {fileExtensionLabel(filename)} + + ) : null} + + ), + }, + { + title: t("knowledgeBases.status"), + key: "status", + width: 100, + render: (_, document) => ( + + + {t( + `knowledgeBases.statusesShort.${document.status}`, + )} + + + ), + }, + { + title: t("knowledgeBases.chunks"), + dataIndex: "chunk_count", + key: "chunk_count", + width: 80, + }, + { + title: t("knowledgeBases.updatedAt"), + dataIndex: "updated_at", + key: "updated_at", + width: 170, + render: (updatedAt: number) => + formatServerDateTime(updatedAt, timeZone), + }, + { + title: t("common.actions"), + key: "actions", + width: canWriteSelected ? 120 : 48, + render: (_, document) => ( +
+ {renderDocumentActions(document)} +
+ ), + }, + ]} + /> + )} + + + )} + + ) : null} + + )} + + setPreviewOpen(false)} + footer={null} + width={720} + destroyOnClose + > + +
{previewText}
+
+
+ + setBaseModalOpen(false)} + onOk={() => void saveBase()} + okText={t(editingBase ? "common.save" : "common.create")} + cancelText={t("common.cancel")} + width={520} + destroyOnClose + className={styles.baseModal} + > +
+ + + + + + + + + +
+
+
+ + {t("knowledgeBases.defaultOpen")} + + + {t("knowledgeBases.defaultOpenHint")} + +
+ +
+
+
+ + {t("knowledgeBases.shared")} + + + {t("knowledgeBases.sharedHint")} + +
+ +
+
+ +
+ + setFeatureModalOpen(false)} + destroyOnHidden + footer={ +
+ + +
+ } + > +
+
+
+ + {t("knowledgeBases.settingsOpen")} + + + {t("knowledgeBases.settingsLead")} + +
+ +
+
+ + {featureEnabledDraft ? ( + +
+
+ {t("knowledgeBases.selectModel")} +
+ { + setFeatureBackend(event.target.value); + setFeatureModel(undefined); + }} + > + {t("knowledgeBases.localOnnx")} + + {t("knowledgeBases.remoteEmbedding")} + + + {featureBackend === "remote" ? ( +
+ { + setSchedulePreset(value); + if (value !== "custom") { + setAutoSchedule(value); + } + }} + /> + + {schedulePreset === "custom" ? ( + + ) : null} + +
+ {autoScheduled + ? t("backup.autoScheduled") + : t("backup.autoNotScheduled")} +
+
+ + +
+
+ + + void | Promise; + isHover: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +function presetModelsToRows(preset: ProviderPreset): ProviderModel[] { + return preset.models.map((m) => ({ + id: m.id, + name: m.name, + enabled: false, + embedding: preset.id === "onnx" ? true : undefined, + task: preset.id === "onnx" ? "embedding" : undefined, + input: m.input?.length ? m.input : ["text"], + thinking: null, + })); +} + +export function LocalServiceCard({ + preset, + provider, + onSaved, + isHover, + onMouseEnter, + onMouseLeave, +}: LocalServiceCardProps) { + const { t } = useTranslation(); + const isOnnx = preset.id === "onnx"; + const [serviceEnabled, setServiceEnabled] = useState(false); + const [serviceRunning, setServiceRunning] = useState(false); + const [serviceBusy, setServiceBusy] = useState(false); + const [depsAvailable, setDepsAvailable] = useState(true); + const [depsInstallFailed, setDepsInstallFailed] = useState(false); + const [ensuring, setEnsuring] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [row, setRow] = useState(provider); + + useEffect(() => { + setRow(provider); + }, [provider]); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + if (isOnnx) { + const st = await onnxModelApi.getStatus(); + if (!cancelled) { + setServiceEnabled(st.enabled); + setServiceRunning(st.ready || st.enabled); + setDepsAvailable(st.deps_available !== false); + setDepsInstallFailed(false); + } + } else { + const st = await ollamaModelApi.getService(); + if (!cancelled) { + setServiceEnabled(st.enabled); + setServiceRunning(st.running); + setDepsAvailable(true); + setDepsInstallFailed(false); + } + } + } catch { + if (!cancelled) { + setServiceEnabled(false); + setServiceRunning(false); + } + } + })(); + return () => { + cancelled = true; + }; + }, [isOnnx, provider?.id]); + + const ensureProvider = async (): Promise => { + if (row) return row; + setEnsuring(true); + try { + const created = await request("/admin/providers", { + method: "POST", + body: JSON.stringify({ + name: preset.name, + kind: preset.protocol, + base_url: preset.base_url || null, + api_key: preset.id, + models: presetModelsToRows(preset), + enabled: false, + }), + }); + setRow(created); + await onSaved(); + return created; + } finally { + setEnsuring(false); + } + }; + + const handleServiceToggle = async (next: boolean) => { + setServiceBusy(true); + try { + const ensured = await ensureProvider(); + if (isOnnx) { + const st = await onnxModelApi.setService(next); + setServiceEnabled(st.enabled); + setServiceRunning(st.ready || st.enabled); + setDepsAvailable(st.deps_available !== false); + setDepsInstallFailed(false); + } else { + const st = await ollamaModelApi.setService(next); + setServiceEnabled(st.enabled); + setServiceRunning(st.running); + } + await request(`/admin/providers/${ensured.id}`, { + method: "PATCH", + body: JSON.stringify({ enabled: next }), + }); + await onSaved(); + message.success( + next + ? t("models.localServiceStarted") + : t("models.localServiceStopped"), + ); + } catch (err) { + if (isOnnx && next) { + setDepsInstallFailed(true); + setDepsAvailable(false); + } + message.error( + err instanceof Error + ? err.message + : t("models.localServiceToggleFailed"), + ); + } finally { + setServiceBusy(false); + } + }; + + const handleOpenSettings = async (e: React.MouseEvent) => { + e.stopPropagation(); + try { + await ensureProvider(); + setModalOpen(true); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("models.createFailedSimple"), + ); + } + }; + + const logo = getProviderLogo(presetLogoId(preset)) ?? customProviderLogo; + const models = row?.models ?? preset.models; + const baseUrl = row?.base_url ?? preset.base_url; + + return ( + <> + +
+
+ + {logo && ( + {preset.name} + )} + {preset.name} + +
+ + + {serviceEnabled + ? serviceRunning + ? t("models.localServiceRunning") + : t("models.localServiceOn") + : t("models.localServiceOff")} + +
+
+ +
+ {!isOnnx && ( +
+ Base URL: + {baseUrl ? ( + + {baseUrl} + + ) : ( + + {t("models.localRuntime")} + + )} +
+ )} +
+ {t("models.model")}: + + {models.length > 0 + ? t("models.modelsCount", { count: models.length }) + : t("models.noModels")} + +
+ {isOnnx && !depsAvailable && ( +
+ + {depsInstallFailed + ? t("models.onnxDepsInstallFailed") + : t("models.onnxDepsPending")} + +
+ )} +
+
+ +
+
+ + void handleServiceToggle(c)} + onClick={(_, e) => e.stopPropagation()} + /> + + + {t("models.localServiceLabel")} + +
+
+ +
+
+
+ + {row && ( + setModalOpen(false)} + onSaved={async () => { + await onSaved(); + }} + apiPrefix="/admin/providers" + /> + )} + + ); +} diff --git a/dashboard/src/pages/Settings/Models/components/cards/PresetProviderCard.tsx b/dashboard/src/pages/Settings/Models/components/cards/PresetProviderCard.tsx index f0f8e22f..00616255 100644 --- a/dashboard/src/pages/Settings/Models/components/cards/PresetProviderCard.tsx +++ b/dashboard/src/pages/Settings/Models/components/cards/PresetProviderCard.tsx @@ -1,18 +1,22 @@ /** * PresetProviderCard — card for a built-in preset provider. * - * Two states: - * - Not configured: gray card with logo, name, protocol badge, and status label - * → click opens PresetProviderModal to create the provider - * - Configured: renders the existing ProviderCard for the matching ProviderRow - * → click opens ProviderConfigModal to edit + * Local runtimes (Ollama / ONNX): always LocalServiceCard — service switch, no create CTA. + * Cloud presets: + * - Not configured → gray card; click opens PresetProviderModal + * - Configured → ProviderCard */ import { useState } from "react"; import { Card, Tag } from "antd"; import { useTranslation } from "react-i18next"; import type { ProviderRow, ProviderPreset } from "../../useProviders"; -import { findConfiguredProvider, presetLogoId } from "../../presetUtils"; +import { + findConfiguredProvider, + isLocalPreset, + presetLogoId, +} from "../../presetUtils"; import { ProviderCard } from "./ProviderCard"; +import { LocalServiceCard } from "./LocalServiceCard"; import { PresetProviderModal } from "../modals/PresetProviderModal"; import { getProviderLogo, @@ -41,12 +45,22 @@ export function PresetProviderCard({ const { t } = useTranslation(); const [modalOpen, setModalOpen] = useState(false); - // Find an existing provider with matching name const configured = findConfiguredProvider(preset, providers); - const logo = getProviderLogo(presetLogoId(preset)) ?? customProviderLogo; - // If already configured, render the real ProviderCard + if (isLocalPreset(preset)) { + return ( + + ); + } + if (configured) { return ( 0; const statusReady = hasApiKey; @@ -145,6 +155,64 @@ export function ProviderCard({ customProviderLogo; const models = provider.models ?? []; + useEffect(() => { + if (!isLocalRuntime) return; + let cancelled = false; + void (async () => { + try { + if (isOllama) { + const st = await ollamaModelApi.getService(); + if (!cancelled) { + setServiceEnabled(st.enabled); + setServiceRunning(st.running); + } + } else if (isOnnx) { + const st = await onnxModelApi.getStatus(); + if (!cancelled) { + setServiceEnabled(st.enabled); + setServiceRunning(st.ready || st.enabled); + } + } + } catch { + if (!cancelled) { + setServiceEnabled(false); + setServiceRunning(false); + } + } + })(); + return () => { + cancelled = true; + }; + }, [isLocalRuntime, isOllama, isOnnx, provider.id]); + + const handleServiceToggle = async (next: boolean) => { + setServiceBusy(true); + try { + if (isOllama) { + const st = await ollamaModelApi.setService(next); + setServiceEnabled(st.enabled); + setServiceRunning(st.running); + } else if (isOnnx) { + const st = await onnxModelApi.setService(next); + setServiceEnabled(st.enabled); + setServiceRunning(st.ready || st.enabled); + } + message.success( + next + ? t("models.localServiceStarted") + : t("models.localServiceStopped"), + ); + } catch (err) { + message.error( + err instanceof Error + ? err.message + : t("models.localServiceToggleFailed"), + ); + } finally { + setServiceBusy(false); + } + }; + return ( <>
+ {isLocalRuntime && ( + <> + + void handleServiceToggle(c)} + onClick={(_, e) => e.stopPropagation()} + /> + + + {serviceEnabled + ? serviceRunning + ? t("models.localServiceRunning") + : t("models.localServiceOn") + : t("models.localServiceOff")} + + + + )}
- {hasApiKey && ( + {hasApiKey && !isOnnx && (
diff --git a/dashboard/src/pages/Settings/Models/components/cards/index.ts b/dashboard/src/pages/Settings/Models/components/cards/index.ts index 41480be1..4c176fb2 100644 --- a/dashboard/src/pages/Settings/Models/components/cards/index.ts +++ b/dashboard/src/pages/Settings/Models/components/cards/index.ts @@ -1,3 +1,4 @@ export * from "./ProviderCard"; export * from "./PresetProviderCard"; export * from "./PresetGroupCard"; +export * from "./LocalServiceCard"; diff --git a/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx b/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx index aee061e5..439db47b 100644 --- a/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx +++ b/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx @@ -9,6 +9,7 @@ import { Download, Zap } from "lucide-react"; import { useTranslation } from "react-i18next"; import { request } from "../../../../../api/request"; import type { ProviderModel, ProviderRow } from "../../useProviders"; +import { isEmbeddingModel } from "../../useProviders"; import { fetchProviderModels, testProviderDraft } from "../../providerApi"; import { ModelListEditor } from "./ModelListEditor"; import styles from "../../index.module.less"; @@ -88,6 +89,7 @@ export function CustomProviderModal({ api_key: key, base_url: values.base_url?.trim() || null, model_id: modelId, + embedding: isEmbeddingModel(models.find((m) => m.id === modelId)), }); }; @@ -164,6 +166,10 @@ export function CustomProviderModal({ if (m.max_tokens != null) entry.max_tokens = m.max_tokens; if (m.context_window != null) entry.context_window = m.context_window; if (m.reasoning) entry.reasoning = true; + if (isEmbeddingModel(m)) { + entry.embedding = true; + entry.task = "embedding"; + } return entry; }); diff --git a/dashboard/src/pages/Settings/Models/components/modals/ModelListEditor.tsx b/dashboard/src/pages/Settings/Models/components/modals/ModelListEditor.tsx index 4d692706..338790ac 100644 --- a/dashboard/src/pages/Settings/Models/components/modals/ModelListEditor.tsx +++ b/dashboard/src/pages/Settings/Models/components/modals/ModelListEditor.tsx @@ -5,13 +5,23 @@ * Connectivity tests still hit the live provider endpoint. */ import { useState } from "react"; -import { Button, Form, Input, InputNumber, Modal, Select, Switch } from "antd"; +import { + Button, + Form, + Input, + InputNumber, + Modal, + Select, + Switch, + Tooltip, +} from "antd"; import { message } from "@/utils/antdMessage"; import { Check, ChevronDown, ChevronUp, + Download, Pencil, Plus, Trash2, @@ -21,9 +31,21 @@ import { import { useTranslation } from "react-i18next"; import { request } from "../../../../../api/request"; import type { ProviderRow, ProviderModel } from "../../useProviders"; +import { isEmbeddingModel } from "../../useProviders"; +import { isOnnxProviderRow } from "../../presetUtils"; import { ModelMetaTags } from "../../modelMeta"; import styles from "../../index.module.less"; +export interface LocalModelDownloadControl { + /** Model ids already present on disk / in the local runtime. */ + downloadedIds: ReadonlySet | readonly string[]; + /** Model ids currently downloading. */ + downloadingIds?: ReadonlySet | readonly string[]; + onDownload: (modelId: string) => void; + /** When true, enable switch is locked until the model is downloaded. */ + requireDownloadToEnable?: boolean; +} + interface ModelListEditorProps { provider: ProviderRow; models: ProviderModel[]; @@ -31,6 +53,7 @@ interface ModelListEditorProps { /** API path prefix for test. Defaults to "/providers". */ apiPrefix?: string; canTest?: boolean; + localDownload?: LocalModelDownloadControl; onTestModel?: ( modelId: string, modelName: string, @@ -49,9 +72,21 @@ export function ModelListEditor({ onModelsChange, apiPrefix = "/providers", canTest, + localDownload, onTestModel, }: ModelListEditorProps) { const { t } = useTranslation(); + const downloadedSet = (() => { + if (!localDownload) return null; + const raw = localDownload.downloadedIds; + return raw instanceof Set ? raw : new Set(raw); + })(); + const downloadingSet = (() => { + if (!localDownload?.downloadingIds) return new Set(); + const raw = localDownload.downloadingIds; + return raw instanceof Set ? raw : new Set(raw); + })(); + const requireDownload = !!localDownload?.requireDownloadToEnable; const [adding, setAdding] = useState(false); const [editingModelId, setEditingModelId] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); @@ -61,12 +96,23 @@ export function ModelListEditor({ >(new Map()); const [testingForm, setTestingForm] = useState(false); const [form] = Form.useForm(); + const isOnnx = isOnnxProviderRow(provider); + const embeddingOn = isOnnx || Form.useWatch("embedding", form) === true; const handleToggleEnabled = ( modelId: string, _modelName: string, enabled: boolean, ) => { + if ( + enabled && + requireDownload && + downloadedSet && + !downloadedSet.has(modelId) + ) { + message.warning(t("models.downloadBeforeEnable")); + return; + } onModelsChange( models.map((m) => (m.id === modelId ? { ...m, enabled } : m)), ); @@ -93,17 +139,21 @@ export function ModelListEditor({ const runTest = async ( modelId: string, modelName: string, + embedding?: boolean, ): Promise<{ ok: boolean; latency_ms?: number; error?: string }> => { if (onTestModel) { return onTestModel(modelId, modelName); } + const isEmbedding = + embedding ?? + (isOnnx || isEmbeddingModel(models.find((m) => m.id === modelId))); return request<{ ok: boolean; latency_ms?: number; error?: string; }>(`${apiPrefix}/${provider.id}/test`, { method: "POST", - body: JSON.stringify({ model_id: modelId }), + body: JSON.stringify({ model_id: modelId, embedding: isEmbedding }), }); }; @@ -157,7 +207,7 @@ export function ModelListEditor({ } setTestingForm(true); try { - const result = await runTest(modelId, modelId); + const result = await runTest(modelId, modelId, embeddingOn); const modelName = (form.getFieldValue("name") as string | undefined)?.trim() || modelId; if (result.ok) { @@ -182,13 +232,23 @@ export function ModelListEditor({ const buildModelEntry = (values: Record): ProviderModel => { const id = (values.id as string).trim(); const name = (values.name as string | undefined)?.trim() || id; + const isOnnx = isOnnxProviderRow(provider); + const embedding = isOnnx || values.embedding === true; const entry: ProviderModel = { id, name, enabled: true, - input: (values.input as string[] | undefined) || ["text"], + input: ["text"], thinking: null, }; + if (embedding) { + entry.embedding = true; + entry.task = "embedding"; + return entry; + } + if (values.input != null) { + entry.input = (values.input as string[] | undefined) || ["text"]; + } if (values.context_window != null) entry.context_window = values.context_window as number; if (values.max_tokens != null) @@ -230,6 +290,9 @@ export function ModelListEditor({ message.error(t("models.initialModelDuplicate", { name: entry.id })); return; } + if (requireDownload) { + entry.enabled = false; + } onModelsChange([...models, entry]); message.success(t("models.modelAdded", { name: entry.name })); resetForm(); @@ -273,6 +336,7 @@ export function ModelListEditor({ reasoning_effort_type: model.reasoning_config?.effort_type ?? "enum", reasoning_adapter: model.reasoning_config?.adapter ?? "thinking", input: model.input ?? ["text"], + embedding: Boolean(model.embedding || model.task === "embedding"), }); const hasAdvanced = (model as Record).context_window != null || @@ -309,6 +373,9 @@ export function ModelListEditor({ const isCurrentEditing = editingModelId === m.id; const isEnabled = m.enabled !== false; const isTesting = testingIds.has(m.id); + const isDownloaded = downloadedSet ? downloadedSet.has(m.id) : true; + const isDownloading = downloadingSet.has(m.id); + const enableLocked = requireDownload && !isDownloaded; return (
- - handleToggleEnabled(m.id, m.name, checked) + + > + + handleToggleEnabled(m.id, m.name, checked) + } + className={styles.modelToggle} + /> +
{m.name} {m.name !== m.id && ( {m.id} )} - + {localDownload && ( + + {isDownloaded + ? t("models.localModelDownloaded") + : t("models.notDownloaded")} + + )} + {(m.embedding || m.task === "embedding") && ( + + {t("models.embeddingOnlyTag")} + + )} + {m.embedding || m.task === "embedding" ? null : ( + + )}
+ {localDownload && !isDownloaded && ( +
- - - - - - - - ) : null - } + + +
+ + + + + + + + ) : null + } + + + )} + )}
([]); const [form] = Form.useForm(); - const isOllama = preset.id === "ollama"; const isCodexOAuth = preset.auth_method === "codex_oauth"; const apiKey = Form.useWatch("api_key", form) as string | undefined; - const canTest = !!(apiKey?.trim() || isOllama); + const canTest = !!apiKey?.trim(); const draftProvider = useMemo( () => ({ @@ -58,21 +59,12 @@ export function PresetProviderModal({ base_url: (form.getFieldValue("base_url") as string | undefined) || preset.base_url, - api_key: - (form.getFieldValue("api_key") as string | undefined) || - (isOllama ? "ollama" : null), + api_key: (form.getFieldValue("api_key") as string | undefined) || null, models: draftModels, note: null, enabled: true, }), - [ - draftModels, - form, - isOllama, - preset.base_url, - preset.name, - preset.protocol, - ], + [draftModels, form, preset.base_url, preset.name, preset.protocol], ); useEffect(() => { @@ -107,15 +99,15 @@ export function PresetProviderModal({ if (isCodexOAuth) return; try { const values = await form.validateFields(["base_url", "api_key"]); - const apiKey = values.api_key?.trim() || (isOllama ? "ollama" : ""); - if (!apiKey) { + const key = values.api_key?.trim(); + if (!key) { message.warning(t("models.pleaseEnterApiKey")); return; } setFetchingModels(true); const result = await fetchProviderModels({ kind: "openai", - api_key: apiKey, + api_key: key, base_url: values.base_url?.trim() || preset.base_url, }); if (!result.ok) { @@ -161,7 +153,7 @@ export function PresetProviderModal({ const testDraftModel = async (modelId: string) => { const values = await form.validateFields(["name", "base_url", "api_key"]); - const key = values.api_key?.trim() || (isOllama ? "ollama" : ""); + const key = values.api_key?.trim(); if (!key) { return { ok: false, error: t("models.pleaseEnterApiKey") }; } @@ -171,6 +163,7 @@ export function PresetProviderModal({ api_key: key, base_url: values.base_url?.trim() || preset.base_url, model_id: modelId, + embedding: isEmbeddingModel(draftModels.find((m) => m.id === modelId)), }); }; @@ -184,8 +177,8 @@ export function PresetProviderModal({ message.warning(t("models.testDraftNeedModel")); return; } - const apiKey = values.api_key?.trim() || (isOllama ? "ollama" : ""); - if (!apiKey) { + const key = values.api_key?.trim(); + if (!key) { message.warning(t("models.pleaseEnterApiKey")); return; } @@ -227,7 +220,7 @@ export function PresetProviderModal({ name: values.name.trim(), kind: preset.protocol, base_url: values.base_url?.trim() || null, - api_key: values.api_key?.trim() || (isOllama ? "ollama" : null), + api_key: values.api_key?.trim() || null, models: draftModels, }), }); @@ -318,17 +311,15 @@ export function PresetProviderModal({ diff --git a/dashboard/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx b/dashboard/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx index a4b7845b..9cde7395 100644 --- a/dashboard/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx +++ b/dashboard/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx @@ -7,15 +7,23 @@ * with local model list, download, and delete UI */ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Button, Divider, Form, Input, Modal, Select } from "antd"; +import { Button, Divider, Form, Input, Modal, Progress, Select } from "antd"; import { message } from "@/utils/antdMessage"; import { Download, Key, Loader2, Trash2, X, Zap } from "lucide-react"; import { useTranslation } from "react-i18next"; import { request } from "../../../../../api/request"; import type { ProviderRow, ProviderModel } from "../../useProviders"; +import { isEmbeddingModel } from "../../useProviders"; import { fetchProviderModels, testProviderDraft } from "../../providerApi"; import { getProviderDocs } from "../../../../../assets/providers"; +import { ollamaModelApi } from "../../../../../api/modules/ollamaModel"; +import { onnxModelApi } from "../../../../../api/modules/onnxModel"; +import { + setOnnxDownloadProgressHandler, + watchOnnxDownload, +} from "../../../../../api/modules/onnxDownloadWatcher"; +import { isOllamaProviderRow, isOnnxProviderRow } from "../../presetUtils"; import { ModelListEditor } from "./ModelListEditor"; import styles from "../../index.module.less"; @@ -60,15 +68,6 @@ function formatFileSize(bytes: number): string { return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`; } -function isOllamaProvider(provider: ProviderRow): boolean { - return ( - provider.name === "ollama" || - provider.name === "Ollama (Local)" || - (provider.base_url?.includes("11434") ?? false) || - (provider.base_url?.includes("ollama") ?? false) - ); -} - export function ProviderConfigModal({ provider, open, @@ -85,7 +84,15 @@ export function ProviderConfigModal({ const [draftModels, setDraftModels] = useState([]); const hasApiKey = !!provider.api_key && provider.api_key.length > 0; - const isOllama = isOllamaProvider(provider); + const isOllama = isOllamaProviderRow(provider); + const isOnnx = isOnnxProviderRow(provider); + const [downloadedIds, setDownloadedIds] = useState([]); + const [downloadingIds, setDownloadingIds] = useState([]); + const [onnxSizeById, setOnnxSizeById] = useState>({}); + const [downloadProgressOpen, setDownloadProgressOpen] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); + const [downloadProgressLabel, setDownloadProgressLabel] = useState(""); + const [downloadProgressModel, setDownloadProgressModel] = useState(""); // === Ollama states === const [downloadForm] = Form.useForm(); @@ -98,6 +105,61 @@ export function ProviderConfigModal({ const ollamaPollRef = useRef | null>(null); const ollamaNotifiedRef = useRef>(new Set()); + const enableModelAfterDownload = useCallback( + async (modelId: string) => { + const currentDefault = ( + form.getFieldValue("model") as string | undefined + )?.trim(); + const nextModels = draftModels.some((m) => m.id === modelId) + ? draftModels.map((m) => + m.id === modelId ? { ...m, enabled: true } : m, + ) + : [ + ...draftModels, + { + id: modelId, + name: modelId, + enabled: true, + ...(isOnnx + ? { embedding: true as const, task: "embedding" as const } + : {}), + input: ["text"], + thinking: null, + }, + ]; + setDraftModels(nextModels); + setFormDirty(true); + const defaultModel = currentDefault || modelId; + if (!currentDefault) { + form.setFieldValue("model", modelId); + } + try { + await request(`${apiPrefix}/${provider.id}`, { + method: "PATCH", + body: JSON.stringify({ + models: nextModels, + model: defaultModel, + }), + }); + if (isOnnx) { + await onnxModelApi.updateConfig({ + enabled: true, + model: modelId, + download_if_missing: false, + }); + } + await onSaved(); + } catch (err) { + message.warning( + err instanceof Error + ? err.message + : t("models.enableAfterDownloadFailed"), + ); + } + }, + [apiPrefix, draftModels, form, isOnnx, onSaved, provider.id, t], + ); + const stopOllamaPolling = useCallback(() => { if (ollamaPollRef.current) { clearInterval(ollamaPollRef.current); @@ -110,13 +172,16 @@ export function ProviderConfigModal({ setOllamaUnavailable(false); try { const data = await request("/ollama-models"); - setOllamaModels(Array.isArray(data) ? data : []); + const list = Array.isArray(data) ? data : []; + setOllamaModels(list); + setDownloadedIds(list.map((m) => m.name)); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (msg.includes("503") || msg.includes("connect")) { setOllamaUnavailable(true); } setOllamaModels([]); + setDownloadedIds([]); } finally { setLoadingOllama(false); } @@ -143,6 +208,7 @@ export function ProviderConfigModal({ if (!ollamaNotifiedRef.current.has(task.task_id)) { ollamaNotifiedRef.current.add(task.task_id); if (task.status === "completed") { + void enableModelAfterDownload(task.name); message.success(t("models.localDownloadSuccess")); needsRefresh = true; } else if (task.status === "cancelled") { @@ -163,7 +229,13 @@ export function ProviderConfigModal({ } catch { /* ignore polling errors */ } - }, [t, onSaved, fetchOllamaModels, stopOllamaPolling]); + }, [ + enableModelAfterDownload, + t, + onSaved, + fetchOllamaModels, + stopOllamaPolling, + ]); const startOllamaPolling = useCallback(() => { if (ollamaPollRef.current) return; @@ -274,6 +346,234 @@ export function ProviderConfigModal({ }); }; + const refreshDownloadedIds = useCallback(async () => { + try { + if (isOllama) { + const data = await request("/ollama-models"); + const list = Array.isArray(data) ? data : []; + setOllamaModels(list); + setDownloadedIds(list.map((m) => m.name)); + setOllamaUnavailable(false); + } else if (isOnnx) { + const st = await onnxModelApi.getStatus(); + setDownloadedIds(st.local_models || []); + } + } catch (err) { + if (isOllama) { + const msg = err instanceof Error ? err.message : String(err); + if ( + msg.includes("503") || + msg.includes("connect") || + msg.includes("disabled") + ) { + setOllamaUnavailable(true); + } + setOllamaModels([]); + setDownloadedIds([]); + } + } + }, [isOllama, isOnnx]); + + const formatSizeGb = useCallback( + (sizeGb?: number | null) => { + if (sizeGb == null || Number.isNaN(sizeGb)) { + return t("models.localDownloadSizeUnknown"); + } + if (sizeGb < 0.01) return `${Math.round(sizeGb * 1024)} MB`; + if (sizeGb < 1) return `${(sizeGb * 1024).toFixed(0)} MB`; + return `${sizeGb.toFixed(2)} GB`; + }, + [t], + ); + + const handleOnnxDownloadTerminal = useCallback( + async (modelId: string, status: string, error?: string | null) => { + setDownloadingIds((prev) => prev.filter((id) => id !== modelId)); + setDownloadProgressOpen(false); + await refreshDownloadedIds(); + if (status === "done") { + await enableModelAfterDownload(modelId); + message.success(t("models.onnxDownloadDone", { model: modelId })); + } else { + message.error(error || t("models.onnxDownloadFailed")); + } + }, + [enableModelAfterDownload, refreshDownloadedIds, t], + ); + + const dismissDownloadProgressToBackground = useCallback(() => { + setDownloadProgressOpen(false); + setOnnxDownloadProgressHandler(undefined); + message.info(t("models.localDownloadBackground")); + }, [t]); + + const runOnnxDownloadWithProgress = useCallback( + async (modelId: string) => { + setDownloadingIds((prev) => + prev.includes(modelId) ? prev : [...prev, modelId], + ); + setDownloadProgressModel(modelId); + setDownloadProgress(0); + setDownloadProgressLabel(t("models.localDownloadPreparing")); + setDownloadProgressOpen(true); + try { + await onnxModelApi.download(modelId); + watchOnnxDownload({ + modelId, + onProgress: (d) => { + const pct = Math.max( + 0, + Math.min(100, Math.round((d.progress || 0) * 100)), + ); + setDownloadProgress(pct); + if (d.status === "loading") { + setDownloadProgressLabel( + t("models.onnxDownloadLoading", { model: modelId }), + ); + } else if (d.status === "downloading") { + setDownloadProgressLabel( + t("models.onnxDownloadProgress", { + model: modelId, + percent: pct, + }), + ); + } + }, + onTerminal: (d) => + handleOnnxDownloadTerminal(modelId, d.status, d.error), + }); + } catch (err) { + setDownloadingIds((prev) => prev.filter((id) => id !== modelId)); + setDownloadProgressOpen(false); + message.error( + err instanceof Error ? err.message : t("models.onnxDownloadFailed"), + ); + } + }, + [handleOnnxDownloadTerminal, t], + ); + + // If progress UI remounts / reopens while a watch is running, re-attach handler. + useEffect(() => { + if (!downloadProgressOpen) return; + setOnnxDownloadProgressHandler((d) => { + const pct = Math.max( + 0, + Math.min(100, Math.round((d.progress || 0) * 100)), + ); + setDownloadProgress(pct); + const modelId = d.model_name || downloadProgressModel; + if (d.status === "loading") { + setDownloadProgressLabel( + t("models.onnxDownloadLoading", { model: modelId }), + ); + } else if (d.status === "downloading") { + setDownloadProgressLabel( + t("models.onnxDownloadProgress", { model: modelId, percent: pct }), + ); + } + }); + return () => setOnnxDownloadProgressHandler(undefined); + }, [downloadProgressOpen, downloadProgressModel, t]); + + // Keep backend watch alive across config-modal close; only stop if still idle. + useEffect(() => { + return () => { + // Do not stopWatchingOnnxDownload on unmount — backend download continues + // and the module-level watcher will still fire onTerminal (toast). + setOnnxDownloadProgressHandler(undefined); + }; + }, []); + + const handleLocalModelDownload = useCallback( + async (modelId: string) => { + if (isOllama) { + Modal.confirm({ + title: t("models.localDownloadConfirmTitle"), + content: t("models.localDownloadConfirmOllama", { name: modelId }), + okText: t("models.localDownloadModel"), + cancelText: t("common.cancel"), + onOk: async () => { + setDownloadingIds((prev) => + prev.includes(modelId) ? prev : [...prev, modelId], + ); + try { + const task = await ollamaModelApi.downloadOllamaModel({ + name: modelId, + }); + setOllamaTasks((prev) => [...prev, task]); + message.info(t("models.localDownloading", { repo: modelId })); + startOllamaPolling(); + } catch (err) { + message.error( + err instanceof Error + ? err.message + : t("models.localDownloadFailed"), + ); + } finally { + setDownloadingIds((prev) => prev.filter((id) => id !== modelId)); + } + }, + }); + return; + } + + if (!isOnnx) return; + + let sizeGb = onnxSizeById[modelId]; + try { + const meta = await onnxModelApi.getModelMeta(modelId); + if (meta.size_gb != null) { + sizeGb = meta.size_gb; + setOnnxSizeById((prev) => ({ ...prev, [modelId]: meta.size_gb! })); + } + } catch { + /* use cached / unknown */ + } + + Modal.confirm({ + title: t("models.localDownloadConfirmTitle"), + content: t("models.localDownloadConfirmOnnx", { + name: modelId, + size: formatSizeGb(sizeGb), + }), + okText: t("models.localDownloadModel"), + cancelText: t("common.cancel"), + onOk: () => { + void runOnnxDownloadWithProgress(modelId); + }, + }); + }, + [ + formatSizeGb, + isOllama, + isOnnx, + onnxSizeById, + runOnnxDownloadWithProgress, + startOllamaPolling, + t, + ], + ); + + useEffect(() => { + if (!open) return; + if (isOllama || isOnnx) { + void refreshDownloadedIds(); + } + if (isOnnx) { + void onnxModelApi + .getCatalog() + .then((items) => { + const map: Record = {}; + for (const it of items || []) { + if (it.size_gb != null) map[it.id] = it.size_gb; + } + setOnnxSizeById(map); + }) + .catch(() => {}); + } + }, [open, isOllama, isOnnx, refreshDownloadedIds]); + // ======================== Form ======================== const apiKeyExtra = useMemo(() => { @@ -429,6 +729,36 @@ export function ProviderConfigModal({ message.warning(t("models.testDraftNeedModel")); return; } + + if (isOnnx) { + if (!downloadedIds.includes(modelId)) { + message.warning(t("models.onnxTestNeedDownload")); + return; + } + const result = await onnxModelApi.test(modelId); + if (result.ok) { + const latency = + result.latency_ms != null + ? t("models.testConnectionLatency", { + time: Math.round(result.latency_ms), + }) + : ""; + message.success( + t("models.testConnectionSuccess", { + name: modelId, + latency, + }), + ); + } else { + message.error( + t("models.testConnectionFailed", { + error: result.error ?? "unknown", + }), + ); + } + return; + } + const draftApiKey = (values.api_key as string | undefined)?.trim(); const draftBaseUrl = (values.base_url as string | undefined)?.trim(); const useDraft = @@ -440,6 +770,9 @@ export function ProviderConfigModal({ return; } + const embedding = isEmbeddingModel( + draftModels.find((m) => m.id === modelId), + ); const result = useDraft || !hasApiKey ? await testProviderDraft({ @@ -448,6 +781,7 @@ export function ProviderConfigModal({ api_key: draftApiKey || provider.api_key || undefined, base_url: draftBaseUrl || provider.base_url, model_id: modelId, + embedding, }) : await request<{ ok: boolean; @@ -455,7 +789,7 @@ export function ProviderConfigModal({ error?: string; }>(`${apiPrefix}/${provider.id}/test`, { method: "POST", - body: JSON.stringify({ model_id: modelId }), + body: JSON.stringify({ model_id: modelId, embedding }), }); if (result.ok) { @@ -487,6 +821,37 @@ export function ProviderConfigModal({ const handleFetchModels = async () => { try { + if (isOnnx) { + setFetchingModels(true); + const catalog = await onnxModelApi.getCatalog(); + const existingIds = new Set(draftModels.map((m) => m.id)); + const missing = (catalog || []).filter((m) => !existingIds.has(m.id)); + if (missing.length === 0) { + message.info(t("models.fetchModelsNoNew")); + return; + } + const added: ProviderModel[] = missing.map((m) => ({ + id: m.id, + name: m.name || m.id, + enabled: false, + embedding: true, + task: "embedding", + input: ["text"], + thinking: null, + })); + setDraftModels((prev) => [...prev, ...added]); + setFormDirty(true); + const sizeMap: Record = {}; + for (const it of catalog || []) { + if (it.size_gb != null) sizeMap[it.id] = it.size_gb; + } + setOnnxSizeById((prev) => ({ ...prev, ...sizeMap })); + message.success( + t("models.fetchModelsMerged", { count: missing.length }), + ); + return; + } + const values = form.getFieldsValue(); const kind = (values.kind as string | undefined) ?? provider.kind; if (kind !== "openai") { @@ -559,7 +924,7 @@ export function ProviderConfigModal({ footer={
- {hasApiKey && ( + {hasApiKey && !isOnnx && ( @@ -593,36 +958,59 @@ export function ProviderConfigModal({ - - - + {!isOnnx && ( + <> + + + - - - + + + + + )} {/* Default model — Select from the models list, or type freely */} {draftModels.length ? ( @@ -647,7 +1035,7 @@ export function ProviderConfigModal({ size="small" icon={} loading={fetchingModels} - onClick={handleFetchModels} + onClick={() => void handleFetchModels()} style={{ marginLeft: 8 }} > {t("models.fetchModels")} @@ -664,8 +1052,46 @@ export function ProviderConfigModal({ models={draftModels} onModelsChange={handleModelsChange} apiPrefix={apiPrefix} + localDownload={ + isOllama || isOnnx + ? { + downloadedIds, + downloadingIds, + onDownload: (id) => void handleLocalModelDownload(id), + requireDownloadToEnable: true, + } + : undefined + } /> + + {t("models.localDownloadContinueBackground")} + + } + > +
+ {downloadProgressLabel || downloadProgressModel} +
+ +
+ {t("models.localDownloadBackgroundHint")} +
+
+ {/* ===== Ollama local models section ===== */} {isOllama && ( <> diff --git a/dashboard/src/pages/Settings/Models/index.tsx b/dashboard/src/pages/Settings/Models/index.tsx index 1892a137..1032c26e 100644 --- a/dashboard/src/pages/Settings/Models/index.tsx +++ b/dashboard/src/pages/Settings/Models/index.tsx @@ -14,6 +14,8 @@ import { Button, Divider, Empty, Space, Tabs, Typography } from "antd"; import { Plus, RefreshCw } from "lucide-react"; import { useTranslation } from "react-i18next"; import PageShell from "../../../layouts/PageShell"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; +import { userCan } from "../../../utils/permissions"; import { useProviders, type ProviderRow } from "./useProviders"; import { groupPresets, isLocalPreset, isPresetProvider } from "./presetUtils"; import type { PresetGroup } from "./presetUtils"; @@ -32,6 +34,10 @@ const { Title } = Typography; export default function ModelsPage() { const { t } = useTranslation(); + const user = useCurrentUser(); + const canProviders = userCan(user, "providers"); + const canOllama = userCan(user, "ollama_models"); + const canOnnx = userCan(user, "onnx_models"); const { providers, presets, @@ -50,9 +56,22 @@ export default function ModelsPage() { [presets], ); const localPresets = useMemo( - () => groupPresets(presets.filter((p) => isLocalPreset(p))), - [presets], + () => + groupPresets( + presets.filter((p) => { + if (!isLocalPreset(p)) return false; + if (p.id === "onnx") return canOnnx; + if (p.id === "ollama") return canOllama; + return canOllama || canOnnx; + }), + ), + [presets, canOllama, canOnnx], ); + const showCloudTab = canProviders; + const showLocalTab = + (canOllama || canOnnx) && + (localPresets.grouped.length > 0 || localPresets.ungrouped.length > 0); + const showPresetSection = showCloudTab || showLocalTab; const renderPresetGrid = ( grouped: PresetGroup[], @@ -139,86 +158,90 @@ export default function ModelsPage() { - {/* Preset providers section */} - {presets.length > 0 && ( + {showPresetSection && ( <> {t("models.presetProviders")} item !== null)} /> )} - {/* Custom providers section — title row with inline "+ add" button */} - <> - {(presets.length > 0 || customProviders.length > 0) && ( - - )} -
- - {t("models.customProviders")} - - -
- {customProviders.length > 0 ? ( -
- {customProviders.map((p) => ( - setHoveredCard(String(p.id))} - onMouseLeave={() => setHoveredCard(null)} - apiPrefix="/admin/providers" - /> - ))} + + {t("models.customProviders")} + +
- ) : ( - - )} - + {customProviders.length > 0 ? ( +
+ {customProviders.map((p) => ( + setHoveredCard(String(p.id))} + onMouseLeave={() => setHoveredCard(null)} + apiPrefix="/admin/providers" + /> + ))} +
+ ) : ( + + )} + + )} )} setAddOpen(false)} onSaved={fetchAll} apiPrefix="/admin/providers" diff --git a/dashboard/src/pages/Settings/Models/presetUtils.ts b/dashboard/src/pages/Settings/Models/presetUtils.ts index 59cd2802..94ddfbad 100644 --- a/dashboard/src/pages/Settings/Models/presetUtils.ts +++ b/dashboard/src/pages/Settings/Models/presetUtils.ts @@ -117,8 +117,51 @@ export function isPresetProvider( ); } -const LOCAL_PRESET_IDS = new Set(["ollama"]); +const LOCAL_PRESET_IDS = new Set(["ollama", "onnx"]); export function isLocalPreset(preset: ProviderPreset): boolean { return LOCAL_PRESET_IDS.has(preset.id); } + +/** Local providers that do not require a real API key. */ +export function isLocalNoKeyPresetId(presetId: string): boolean { + return presetId === "ollama" || presetId === "onnx"; +} + +/** Stable placeholder api_key written when creating a local preset row. */ +function localPresetApiKey(provider: { api_key?: string | null }): string { + return (provider.api_key ?? "").trim().toLowerCase(); +} + +export function isOnnxProviderRow(provider: { + name: string; + api_key?: string | null; +}): boolean { + if (localPresetApiKey(provider) === "onnx") return true; + const n = provider.name.toLowerCase(); + return n === "onnx" || n === "onnx (local)"; +} + +export function isOllamaProviderRow(provider: { + name: string; + base_url?: string | null; + api_key?: string | null; +}): boolean { + if (isOnnxProviderRow(provider)) return false; + if (localPresetApiKey(provider) === "ollama") return true; + const n = provider.name.toLowerCase(); + return ( + n === "ollama" || + n === "ollama (local)" || + (provider.base_url?.includes("11434") ?? false) || + (provider.base_url?.includes("ollama") ?? false) + ); +} + +export function isLocalProviderRow(provider: { + name: string; + base_url?: string | null; + api_key?: string | null; +}): boolean { + return isOnnxProviderRow(provider) || isOllamaProviderRow(provider); +} diff --git a/dashboard/src/pages/Settings/Models/providerApi.ts b/dashboard/src/pages/Settings/Models/providerApi.ts index 8c33d51a..343f9902 100644 --- a/dashboard/src/pages/Settings/Models/providerApi.ts +++ b/dashboard/src/pages/Settings/Models/providerApi.ts @@ -7,6 +7,7 @@ export interface TestProviderDraftParams { base_url?: string | null; model_id: string; extra_json?: string | null; + embedding?: boolean; } export interface TestProviderResult { @@ -27,6 +28,7 @@ export async function testProviderDraft( base_url: params.base_url?.trim() || null, model_id: params.model_id, extra_json: params.extra_json ?? null, + embedding: params.embedding === true, }), }); } diff --git a/dashboard/src/pages/Settings/Models/useProviders.ts b/dashboard/src/pages/Settings/Models/useProviders.ts index 39176b1c..5c188178 100644 --- a/dashboard/src/pages/Settings/Models/useProviders.ts +++ b/dashboard/src/pages/Settings/Models/useProviders.ts @@ -22,6 +22,16 @@ export interface ProviderModel { reasoning_config?: ResolvedModel["reasoning_config"]; context_window?: number; max_tokens?: number; + /** Embedding-only: excluded from chat picker and auto-route. */ + embedding?: boolean; + task?: string; +} + +export function isEmbeddingModel( + model: Pick | undefined, +): boolean { + if (!model) return false; + return model.embedding === true || model.task === "embedding"; } export interface ProviderPresetModel { @@ -99,29 +109,49 @@ export function useProviders(): UseProvidersResult { if (!hasLoadedRef.current) setLoading(true); setError(null); try { - const [rows, presetList, resolved, active, preferences] = - await Promise.all([ - request("/admin/providers"), - request("/providers/presets"), - providerApi.listResolvedModels(), - request<{ provider_name: string; model: string }>( - "/providers/active-model", - ), - preferencesApi.get(), - ]); - if (!Array.isArray(rows)) { - throw new Error( - "Unexpected API response shape from /admin/providers — is BASE_URL configured correctly?", - ); + const settled = await Promise.allSettled([ + request("/admin/providers"), + request("/providers/presets"), + providerApi.listResolvedModels(), + request<{ provider_name: string; model: string }>( + "/providers/active-model", + ), + preferencesApi.get(), + ]); + const rows = + settled[0].status === "fulfilled" && Array.isArray(settled[0].value) + ? settled[0].value + : []; + const presetList = + settled[1].status === "fulfilled" && Array.isArray(settled[1].value) + ? settled[1].value + : []; + const resolved = + settled[2].status === "fulfilled" && Array.isArray(settled[2].value) + ? settled[2].value + : []; + const active = + settled[3].status === "fulfilled" ? settled[3].value : null; + const preferences = + settled[4].status === "fulfilled" ? settled[4].value : null; + + if ( + settled[0].status === "rejected" && + settled[1].status === "rejected" + ) { + throw settled[0].reason instanceof Error + ? settled[0].reason + : new Error(t("models.loadProvidersFailed")); } + const normalized = rows.map((r) => ({ ...r, models: Array.isArray(r.models) ? r.models : [], })); setProviders(normalized); - setPresets(Array.isArray(presetList) ? presetList : []); - setResolvedModels(Array.isArray(resolved) ? resolved : []); - const preferredParts = (preferences.preferred_model || "").split("/"); + setPresets(presetList); + setResolvedModels(resolved); + const preferredParts = (preferences?.preferred_model || "").split("/"); setActiveModel( preferredParts.length > 1 ? { @@ -135,7 +165,7 @@ export function useProviders(): UseProvidersResult { } : { provider_name: "", model: "" }, ); - setModelReasoning(preferences.model_reasoning || {}); + setModelReasoning(preferences?.model_reasoning || {}); } catch (err) { const msg = err instanceof Error ? err.message : t("models.loadProvidersFailed"); diff --git a/dashboard/src/pages/Settings/Security/HitlToolsPicker.tsx b/dashboard/src/pages/Settings/Security/HitlToolsPicker.tsx new file mode 100644 index 00000000..2861e383 --- /dev/null +++ b/dashboard/src/pages/Settings/Security/HitlToolsPicker.tsx @@ -0,0 +1,114 @@ +import { useMemo } from "react"; +import { Button, Checkbox, Space, Typography } from "antd"; +import { useTranslation } from "react-i18next"; +import type { HitlToolCatalogItem } from "../../../api/modules/security"; +import styles from "./index.module.less"; + +const { Text } = Typography; + +function localizedLabel(item: HitlToolCatalogItem, locale: string): string { + return locale.startsWith("zh") ? item.label_zh : item.label_en; +} + +interface HitlToolsPickerProps { + catalog: HitlToolCatalogItem[]; + defaultTools?: string[]; + value?: string[]; + onChange?: (tools: string[]) => void; + disabled?: boolean; +} + +export default function HitlToolsPicker({ + catalog, + defaultTools = [], + value = [], + onChange, + disabled, +}: HitlToolsPickerProps) { + const { t, i18n } = useTranslation(); + + const options = useMemo(() => { + const known = new Set(catalog.map((item) => item.name)); + const extras = value.filter((name) => !known.has(name)); + return [ + ...catalog, + ...extras.map((name) => ({ + name, + label_zh: name, + label_en: name, + })), + ]; + }, [catalog, value]); + + const handleSelectAll = () => { + onChange?.(options.map((item) => item.name)); + }; + + const handleDeselectAll = () => { + onChange?.([]); + }; + + const handleSelectDefaults = () => { + const known = new Set(options.map((item) => item.name)); + onChange?.(defaultTools.filter((name) => known.has(name))); + }; + + if (options.length === 0) { + return {t("security.hitlToolsEmpty")}; + } + + return ( +
+
+ + {t("security.hitlToolsPickerHint")} + + + + + + +
+ onChange?.(names as string[])} + > + {options.map((item) => ( + + + {localizedLabel(item, i18n.language)} + + {item.name} + + ))} + +
+ ); +} diff --git a/dashboard/src/pages/Settings/Security/index.module.less b/dashboard/src/pages/Settings/Security/index.module.less index a83d9f8f..e4971d54 100644 --- a/dashboard/src/pages/Settings/Security/index.module.less +++ b/dashboard/src/pages/Settings/Security/index.module.less @@ -15,22 +15,28 @@ .footer { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: 12px 20px; margin-top: 8px; padding-top: 20px; border-top: 1px solid var(--fn-border-primary); - max-width: 720px; + width: 100%; } .runtimeHint { flex: 1; - min-width: 200px; + min-width: 0; font-size: 12px; line-height: 1.5; } +@media (max-width: 767px) { + .footer { + flex-wrap: wrap; + } +} + .rulesSection { margin-top: 28px; padding-top: 24px; @@ -109,6 +115,70 @@ gap: 8px; } -.rulesTable { +.hitlToolsSection { + width: 100%; + margin-top: 4px; +} + +.hitlToolsPicker { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; +} + +.hitlToolsPickerHeader { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 8px 16px; +} + +.hitlToolsPickerHint { + flex: 1; + min-width: 220px; + font-size: 12px; + line-height: 1.5; +} + +.hitlToolsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); + gap: 8px; width: 100%; } + +.hitlToolTile { + align-items: flex-start !important; + margin: 0 !important; + padding: 10px 12px; + border: 1px solid var(--fn-border-primary); + border-radius: var(--fn-radius-md, 8px); + background: var(--fn-bg-layout, var(--fn-bg-page, transparent)); + transition: border-color 0.15s ease; + + &:hover { + border-color: var(--fn-border-secondary, var(--fn-border-primary)); + } + + :global(.ant-checkbox) { + margin-top: 2px; + } +} + +.hitlToolLabel { + display: block; + font-size: 13px; + line-height: 1.45; + color: var(--fn-text-primary); +} + +.hitlToolName { + display: block; + margin-top: 2px; + font-size: 11px; + line-height: 1.35; + color: var(--fn-text-tertiary, var(--fn-text-secondary)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} diff --git a/dashboard/src/pages/Settings/Security/index.tsx b/dashboard/src/pages/Settings/Security/index.tsx index 8765fa1c..e2ed4384 100644 --- a/dashboard/src/pages/Settings/Security/index.tsx +++ b/dashboard/src/pages/Settings/Security/index.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useState, type ReactNode } from "react"; -import { useSearchParams } from "react-router-dom"; import { Button, Form, Input, Modal, Select, Switch, Typography } from "antd"; import { message } from "@/utils/antdMessage"; import { @@ -16,21 +15,24 @@ import { apiErrorMessage } from "../../../utils/apiError"; import { securityApi, type FilesystemRule, + type HitlToolCatalogItem, type SecurityPolicy, } from "../../../api/modules/security"; import { TabPanelHeader } from "../AdvancedSettings/TabPanelHeader"; import tabStyles from "../AdvancedSettings/tabContent.module.less"; import SettingsTabBar from "../shared/SettingsTabBar"; import AuditLogPanel from "./AuditLogPanel"; +import HitlToolsPicker from "./HitlToolsPicker"; import ToolGuardRulesPanel from "./ToolGuardRulesPanel"; import styles from "./index.module.less"; +import ForbiddenPage from "../../../components/ForbiddenPage"; +import { useGatedSearchTabs } from "../../../hooks/useGatedSearchTabs"; +import { SECURITY_TAB_PERMISSIONS, userCan } from "../../../utils/permissions"; const { Text } = Typography; const { confirm } = Modal; const { TextArea } = Input; -const DEFAULT_HITL_TOOLS = ["bash", "execute", "write_file", "edit_file"]; - type SecurityTabKey = | "hitl" | "filesystem" @@ -125,36 +127,36 @@ function PolicyFooter({ export default function SecuritySettingsPage() { const { t } = useTranslation(); - const [searchParams, setSearchParams] = useSearchParams(); - const [activeTab, setActiveTab] = useState(() => - parseTab(searchParams.get("tab")), - ); + const { user, allowedTabs, activeTab, forbidden, selectTab } = + useGatedSearchTabs({ + tabs: TABS, + tabPermissions: SECURITY_TAB_PERMISSIONS, + parseTab, + querylessKey: "hitl", + }); const [form] = Form.useForm(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [policy, setPolicy] = useState(null); - - useEffect(() => { - setActiveTab(parseTab(searchParams.get("tab"))); - }, [searchParams]); - - const selectTab = (key: SecurityTabKey) => { - setActiveTab(key); - if (key === "hitl") { - searchParams.delete("tab"); - setSearchParams(searchParams, { replace: true }); - } else { - setSearchParams({ tab: key }, { replace: true }); - } - }; + const [hitlToolCatalog, setHitlToolCatalog] = useState( + [], + ); + const [hitlDefaultTools, setHitlDefaultTools] = useState([]); const fetchPolicy = useCallback(async () => { setLoading(true); try { - const cfg = await securityApi.getPolicy(); + const [cfg, defaults] = await Promise.all([ + securityApi.getPolicy(), + securityApi.getDefaults(), + ]); setPolicy(cfg); + setHitlToolCatalog(defaults.hitl_tool_catalog); + setHitlDefaultTools([...defaults.hitl_tools]); const tools = - cfg.hitl.tools === "default" ? [...DEFAULT_HITL_TOOLS] : cfg.hitl.tools; + cfg.hitl.tools === "default" + ? [...defaults.hitl_tools] + : cfg.hitl.tools; form.setFieldsValue({ hitl_enabled: cfg.hitl.enabled, hitl_tools: tools, @@ -175,8 +177,12 @@ export default function SecuritySettingsPage() { }, [form, t]); useEffect(() => { - void fetchPolicy(); - }, [fetchPolicy]); + if (userCan(user, "security")) { + void fetchPolicy(); + return; + } + setLoading(false); + }, [fetchPolicy, user]); const handleSave = async () => { try { @@ -259,14 +265,12 @@ export default function SecuritySettingsPage() { }} />
+
+
- { - const name = String(option?.value ?? ""); - const label = t(`experts.iconLabels.${name}`, { - defaultValue: name, - }); - const q = input.trim().toLowerCase(); - return ( - name.toLowerCase().includes(q) || - label.toLowerCase().includes(q) - ); - }} - options={EXPERT_ICON_NAMES.map((name) => ({ - value: name, - label: ( - - {iconForName(name, 18)} - - {t(`experts.iconLabels.${name}`, { defaultValue: name })} - - - ), - }))} - /> + { +describe("pathPermissionKeys", () => { it("matches workbench and legacy aliases", () => { expect(isWorkbenchPath("/workbench")).toBe(true); - expect(isControlAdminPath("/workbench")).toBe(true); - expect(isControlAdminPath("/workbench/terminal")).toBe(true); - expect(isControlAdminPath("/workbench/browser")).toBe(true); - expect(isControlAdminPath("/terminal")).toBe(true); - expect(isControlAdminPath("/remote-browser")).toBe(true); + expect(pathPermissionKeys("/workbench")).toEqual([...PERM.workbench]); + expect(pathPermissionKeys("/workbench/terminal")).toEqual([ + ...PERM.terminal, + ]); + expect(pathPermissionKeys("/workbench/browser")).toEqual([...PERM.browser]); + expect(pathPermissionKeys("/terminal")).toEqual([...PERM.terminal]); + expect(pathPermissionKeys("/remote-browser")).toEqual([...PERM.browser]); }); it("matches remote desktop and acp", () => { - expect(isControlAdminPath("/remote-desktop")).toBe(true); - expect(isControlAdminPath("/acp")).toBe(true); + expect(pathPermissionKeys("/remote-desktop")).toEqual([...PERM.desktop]); + expect(pathPermissionKeys("/acp")).toBe("admin"); }); - it("does not match settings or common pages", () => { - expect(isControlAdminPath("/chat")).toBe(false); - expect(isControlAdminPath("/connectors")).toBe(false); - expect(isControlAdminPath("/personalization/skills")).toBe(false); - expect(isControlAdminPath("/skill-packages")).toBe(false); - expect(isControlAdminPath("/admin/users")).toBe(false); + it("keeps sso on users page, not advanced", () => { + expect(pathPermissionKeys("/admin/users")).toEqual([...PERM.usersPage]); + expect(pathPermissionKeys("/admin/advanced")).toEqual([ + ...PERM.advancedPage, + ]); + expect([...PERM.advancedPage]).not.toContain("sso"); + expect(NAV_PERMISSIONS["admin-users"]).toEqual(PERM.usersPage); + expect(NAV_PERMISSIONS["admin-advanced"]).toEqual(PERM.advancedPage); + }); + + it("does not gate common pages", () => { + expect(pathPermissionKeys("/chat")).toBeNull(); + expect(pathPermissionKeys("/experts")).toBeNull(); + expect(pathPermissionKeys("/tasks")).toBeNull(); + expect(pathPermissionKeys("/token-usage")).toBeNull(); + expect(pathPermissionKeys("/personalization/skills")).toBeNull(); + }); + + it("gates settings modules", () => { + expect(pathPermissionKeys("/connectors")).toEqual([...PERM.connectors]); + expect(pathPermissionKeys("/skill-packages")).toEqual([ + ...PERM.skillPackages, + ]); + expect(pathPermissionKeys("/personalization/channels")).toEqual([ + ...PERM.channels, + ]); + expect(pathPermissionKeys("/knowledge-bases")).toEqual([ + ...PERM.knowledgeBasesPage, + ]); + expect([...PERM.advancedPage]).not.toContain("knowledge_settings"); + }); + + it("canAccessPath respects holder permissions", () => { + const user = { role: "user", permissions: ["desktop"] }; + expect(canAccessPath(user, "/remote-desktop")).toBe(true); + expect(canAccessPath(user, "/admin/users")).toBe(false); + expect(canAccessPath({ role: "admin", permissions: [] }, "/acp")).toBe( + true, + ); + expect( + canAccessPath( + { role: "user", permissions: ["knowledge_bases"] }, + "/knowledge-bases", + ), + ).toBe(true); + expect( + canAccessPath( + { role: "user", permissions: ["knowledge_settings"] }, + "/knowledge-bases", + ), + ).toBe(true); + expect( + canAccessPath( + { role: "user", permissions: ["knowledge_bases"] }, + "/admin/advanced", + ), + ).toBe(false); }); }); diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index 47e94234..eb503907 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -7,6 +7,7 @@ const CronJobsPage = lazy(() => import("../pages/Control/CronJobs")); const ConnectorsPage = lazy(() => import("../pages/Agent/Connectors")); const ACPPage = lazy(() => import("../pages/Agent/ACP")); const SkillPackagesPage = lazy(() => import("../pages/SkillPackages")); +const KnowledgeBasesPage = lazy(() => import("../pages/KnowledgeBases")); const PersonalizationPage = lazy( () => import("../pages/Agent/Personalization"), ); @@ -50,6 +51,7 @@ export const pathToKey: Record = { "/tasks": "tasks", "/connectors": "connectors", "/skill-packages": "skill-packages", + "/knowledge-bases": "knowledge-bases", "/acp": "acp", "/personalization": "personalization", "/personalization/skills": "personalization", @@ -104,24 +106,6 @@ export function isWorkbenchPath(pathname: string): boolean { return pathname === "/workbench" || pathname.startsWith("/workbench/"); } -/** - * Control-group pages (Workbench / Remote Desktop / ACP) — admin only. - * Includes legacy aliases that redirect into Workbench. - * Shared by Sidebar visibility and RequireAdmin route guards. - */ -export function isControlAdminPath(pathname: string): boolean { - if (isWorkbenchPath(pathname)) return true; - if (pathname === "/terminal" || pathname === "/remote-browser") return true; - if ( - pathname === "/remote-desktop" || - pathname.startsWith("/remote-desktop/") - ) { - return true; - } - if (pathname === "/acp" || pathname.startsWith("/acp/")) return true; - return false; -} - export function isPersonalizationPath(pathname: string): boolean { return ( pathname === "/personalization" || pathname.startsWith("/personalization/") @@ -147,6 +131,7 @@ export const routeConfigs: RouteConfig[] = [ { path: "/tasks", element: }, { path: "/connectors", element: }, { path: "/skill-packages", element: }, + { path: "/knowledge-bases", element: }, { path: "/personalization/*", element: }, { path: "/skills", @@ -154,7 +139,7 @@ export const routeConfigs: RouteConfig[] = [ }, { path: "/token-usage", element: }, - // Control (admin-only UI; RequireAdmin via isControlAdminPath in MainLayout) + // Control (RequirePermission via pathPermissionKeys in MainLayout) { path: "/acp", element: }, { path: "/channels", @@ -190,8 +175,12 @@ export const routeConfigs: RouteConfig[] = [ // Settings { path: "/admin/models", element: }, - // Admin (RequireAdmin wrapper applied in MainLayout — Task 10) + // Admin (RequirePermission wrapper applied in MainLayout) { path: "/admin/users", element: }, + { + path: "/admin/sso", + element: , + }, { path: "/admin/shared-models", element: , diff --git a/dashboard/src/routes/prefetch.ts b/dashboard/src/routes/prefetch.ts index 3ecd894f..f7fbd4b9 100644 --- a/dashboard/src/routes/prefetch.ts +++ b/dashboard/src/routes/prefetch.ts @@ -5,6 +5,7 @@ const ROUTE_PREFETCHERS: Record Promise> = { "/tasks": () => import("../pages/Control/CronJobs"), "/connectors": () => import("../pages/Agent/Connectors"), "/skill-packages": () => import("../pages/SkillPackages"), + "/knowledge-bases": () => import("../pages/KnowledgeBases"), "/skills": () => import("../pages/Agent/Personalization"), "/token-usage": () => import("../pages/Control/TokenUsage"), "/channels": () => import("../pages/Agent/Personalization"), diff --git a/dashboard/src/utils/apiError.ts b/dashboard/src/utils/apiError.ts index 28480136..7db7419c 100644 --- a/dashboard/src/utils/apiError.ts +++ b/dashboard/src/utils/apiError.ts @@ -47,10 +47,16 @@ export function parseApiError(error: unknown): ParsedApiError | null { return null; } +const NOT_FOUND_ERROR_CODES = new Set([ + "NOT_FOUND", + "KNOWLEDGE_NOT_FOUND", + "SKILL_PACKAGE_NOT_FOUND", +]); + /** True when a failed ``request`` / ``requestBlob`` looks like HTTP 404 / NOT_FOUND. */ export function isNotFoundApiError(error: unknown): boolean { const parsed = parseApiError(error); - if (parsed?.code === "NOT_FOUND") return true; + if (parsed?.code && NOT_FOUND_ERROR_CODES.has(parsed.code)) return true; const msg = error instanceof Error ? error.message : String(error ?? ""); return /\b404\b/i.test(msg) || /not found/i.test(msg); } diff --git a/dashboard/src/pages/SkillPackages/detailRequestGate.test.ts b/dashboard/src/utils/detailRequestGate.test.ts similarity index 100% rename from dashboard/src/pages/SkillPackages/detailRequestGate.test.ts rename to dashboard/src/utils/detailRequestGate.test.ts diff --git a/dashboard/src/pages/SkillPackages/detailRequestGate.ts b/dashboard/src/utils/detailRequestGate.ts similarity index 77% rename from dashboard/src/pages/SkillPackages/detailRequestGate.ts rename to dashboard/src/utils/detailRequestGate.ts index 11e0e086..2e14ceb6 100644 --- a/dashboard/src/pages/SkillPackages/detailRequestGate.ts +++ b/dashboard/src/utils/detailRequestGate.ts @@ -1,3 +1,4 @@ +/** Invalidate in-flight detail fetches after delete / re-select. */ export function createDetailRequestGate() { let latestRequest = 0; diff --git a/dashboard/src/utils/embeddingDownload.ts b/dashboard/src/utils/embeddingDownload.ts index dd49f695..c14aa3af 100644 --- a/dashboard/src/utils/embeddingDownload.ts +++ b/dashboard/src/utils/embeddingDownload.ts @@ -21,6 +21,18 @@ export function formatBytes(value?: number): string { return `${current.toFixed(digits)} ${units[unitIndex]}`; } +/** Approximate ONNX catalog size from gigabytes (fastembed `size_in_GB`). */ +export function formatSizeGb(sizeGb?: number | null): string | null { + if (sizeGb == null || Number.isNaN(sizeGb) || sizeGb < 0) { + return null; + } + if (sizeGb < 1) { + const mb = sizeGb * 1024; + return `${mb >= 10 ? mb.toFixed(0) : mb.toFixed(1)} MB`; + } + return `${sizeGb < 10 ? sizeGb.toFixed(1) : sizeGb.toFixed(0)} GB`; +} + export function formatDuration(seconds?: number): string { if (seconds === undefined || seconds === null || Number.isNaN(seconds)) { return "0s"; diff --git a/dashboard/src/utils/expertColor.test.ts b/dashboard/src/utils/expertColor.test.ts new file mode 100644 index 00000000..3d35bd28 --- /dev/null +++ b/dashboard/src/utils/expertColor.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_SUBAGENT_ACCENT, + expertPaletteColor, + resolveExpertPalette, + resolveSubagentAccent, +} from "./expertColor"; + +describe("resolveExpertPalette", () => { + it("matches exact curated swatches", () => { + expect(resolveExpertPalette("#E85D75")).toBe("rose"); + expect(resolveExpertPalette("#6366F1")).toBe("indigo"); + }); + + it("falls back to rose when color is missing", () => { + expect(resolveExpertPalette(null)).toBe("rose"); + expect(resolveExpertPalette(undefined)).toBe("rose"); + }); + + it("snaps nearby template pastels onto the nearest swatch", () => { + expect(resolveExpertPalette("#e8f4ff")).toBe("indigo"); + }); + + it("returns the hex for a palette key", () => { + expect(expertPaletteColor("amber")).toBe("#F59E0B"); + }); +}); + +describe("resolveSubagentAccent", () => { + it("returns hex as-is", () => { + expect(resolveSubagentAccent("#4B74FA")).toBe("#4B74FA"); + }); + + it("maps curated palette keys onto swatch hex", () => { + expect(resolveSubagentAccent("tech")).toBe(expertPaletteColor("tech")); + expect(resolveSubagentAccent("Rose")).toBe(expertPaletteColor("rose")); + }); + + it("keeps CSS named colors for catalog frontmatter", () => { + expect(resolveSubagentAccent("orange")).toBe("orange"); + expect(resolveSubagentAccent("blue")).toBe("blue"); + }); + + it("falls back when color is missing or unsafe", () => { + expect(resolveSubagentAccent(null)).toBe(DEFAULT_SUBAGENT_ACCENT); + expect(resolveSubagentAccent("")).toBe(DEFAULT_SUBAGENT_ACCENT); + expect(resolveSubagentAccent("red; background: red")).toBe( + DEFAULT_SUBAGENT_ACCENT, + ); + }); +}); diff --git a/dashboard/src/utils/expertColor.ts b/dashboard/src/utils/expertColor.ts new file mode 100644 index 00000000..0e155ee9 --- /dev/null +++ b/dashboard/src/utils/expertColor.ts @@ -0,0 +1,101 @@ +import { + DEFAULT_PALETTE, + PALETTE_SWATCH, + VALID_PALETTES, + type ThemePalette, +} from "../styles/themePalettes"; + +function parseHex(color: string): [number, number, number] | null { + const raw = color.trim().replace(/^#/, ""); + if (!/^[0-9a-fA-F]{6}$/.test(raw) && !/^[0-9a-fA-F]{3}$/.test(raw)) { + return null; + } + const hex = + raw.length === 3 + ? raw + .split("") + .map((ch) => `${ch}${ch}`) + .join("") + : raw; + return [ + Number.parseInt(hex.slice(0, 2), 16), + Number.parseInt(hex.slice(2, 4), 16), + Number.parseInt(hex.slice(4, 6), 16), + ]; +} + +function colorDistance( + a: [number, number, number], + b: [number, number, number], +): number { + const dr = a[0] - b[0]; + const dg = a[1] - b[1]; + const db = a[2] - b[2]; + return dr * dr + dg * dg + db * db; +} + +/** Map a stored expert hex (or null) onto the curated 8-swatch palette. */ +export function resolveExpertPalette( + color: string | null | undefined, +): ThemePalette { + if (!color) return DEFAULT_PALETTE; + const normalized = color.trim().toLowerCase(); + for (const key of VALID_PALETTES) { + if (PALETTE_SWATCH[key].toLowerCase() === normalized) { + return key; + } + } + const rgb = parseHex(normalized); + if (!rgb) return DEFAULT_PALETTE; + + let best: ThemePalette = DEFAULT_PALETTE; + let bestDist = Number.POSITIVE_INFINITY; + for (const key of VALID_PALETTES) { + const swatch = parseHex(PALETTE_SWATCH[key]); + if (!swatch) continue; + const dist = colorDistance(rgb, swatch); + if (dist < bestDist) { + best = key; + bestDist = dist; + } + } + return best; +} + +export function expertPaletteColor(palette: ThemePalette): string { + return PALETTE_SWATCH[palette]; +} + +/** Fallback accent when a subagent has no usable color. */ +export const DEFAULT_SUBAGENT_ACCENT = PALETTE_SWATCH.indigo; + +/** + * Resolve a subagent frontmatter color for card chrome. + * + * Accepts curated palette keys (`rose`), hex (`#4B74FA`), and CSS named + * colors (`orange`). Anything else falls back to indigo. + */ +export function resolveSubagentAccent( + color: string | null | undefined, +): string { + const raw = (color ?? "").trim().replace(/^["']|["']$/g, ""); + if (!raw) return DEFAULT_SUBAGENT_ACCENT; + const lower = raw.toLowerCase(); + if ((VALID_PALETTES as readonly string[]).includes(lower)) { + return PALETTE_SWATCH[lower as ThemePalette]; + } + if (raw.startsWith("#") && parseHex(raw)) return raw; + if (/^[a-zA-Z][\w-]*$/.test(raw)) return raw; + return DEFAULT_SUBAGENT_ACCENT; +} + +/** Icon chip background that works for both hex and named CSS colors. */ +export function subagentAccentIconStyle(accent: string): { + color: string; + background: string; +} { + return { + color: accent, + background: `color-mix(in srgb, ${accent} 10%, transparent)`, + }; +} diff --git a/dashboard/src/utils/fileTreeIcon.tsx b/dashboard/src/utils/fileTreeIcon.tsx index 9a8897ab..2ea626c6 100644 --- a/dashboard/src/utils/fileTreeIcon.tsx +++ b/dashboard/src/utils/fileTreeIcon.tsx @@ -184,3 +184,8 @@ export function fileTreeIcon(path: string, size = 13) { const { Icon, color } = resolveSpec(path); return ; } + +/** Icon component + color for a path (useful for card badges). */ +export function fileTreeIconSpec(path: string): IconSpec { + return resolveSpec(path); +} diff --git a/dashboard/src/utils/permissions.ts b/dashboard/src/utils/permissions.ts new file mode 100644 index 00000000..7eee1cb4 --- /dev/null +++ b/dashboard/src/utils/permissions.ts @@ -0,0 +1,224 @@ +/** Client-side helpers mirroring backend ``user_has_permission``. */ + +export type PermissionHolder = { + role: "admin" | "user" | string; + permissions?: string[] | null; +}; + +/** Any-of module keys, or ``"admin"`` for role-only. */ +export type PermissionKeys = readonly string[] | "admin"; + +export const PERM = { + channels: ["channels"], + connectors: ["connectors"], + skillPackages: ["skill_packages"], + knowledgeBases: ["knowledge_bases"], + knowledgeSettings: ["knowledge_settings"], + knowledgeBasesPage: ["knowledge_bases", "knowledge_settings"], + workbench: ["browser", "terminal"], + browser: ["browser"], + terminal: ["terminal"], + desktop: ["desktop"], + usersPage: ["users", "sso"], + modelsPage: ["providers", "ollama_models", "onnx_models"], + storage: ["storage_backends"], + plugins: ["plugins"], + securityPage: ["security", "admin_console"], + advancedPage: [ + "voice", + "update", + "envs", + "tls", + "observability", + "backup", + "search", + ], +} as const satisfies Record; + +/** Sidebar item key → permission keys. Shared with path guards. */ +export const NAV_PERMISSIONS = { + channels: PERM.channels, + connectors: PERM.connectors, + "skill-packages": PERM.skillPackages, + "knowledge-bases": PERM.knowledgeBasesPage, + workbench: PERM.workbench, + "remote-desktop": PERM.desktop, + acp: "admin", + "admin-users": PERM.usersPage, + models: PERM.modelsPage, + "admin-storage": PERM.storage, + "admin-plugins": PERM.plugins, + "admin-security": PERM.securityPage, + "admin-advanced": PERM.advancedPage, +} as const satisfies Record; + +export type NavPermissionKey = keyof typeof NAV_PERMISSIONS; + +export const USERS_TAB_PERMISSIONS = { + local: "users", + sso: "sso", +} as const; + +export const ADVANCED_TAB_PERMISSIONS = { + "env-vars": "envs", + search: "search", + voice: "voice", + observability: "observability", + backup: "backup", + https: "tls", + updates: "update", +} as const; + +export const SECURITY_TAB_PERMISSIONS = { + hitl: "security", + filesystem: "security", + pii: "security", + tool_guard: "security", + skill_scan: "security", + audit: "admin_console", +} as const; + +/** True when the user may access the module ``key`` (admin bypasses). */ +export function userCan( + user: PermissionHolder | null | undefined, + key: string, +): boolean { + if (!user) return false; + if (user.role === "admin") return true; + return (user.permissions ?? []).includes(key); +} + +/** True when the user holds any of the given keys (admin bypasses). */ +export function userCanAny( + user: PermissionHolder | null | undefined, + keys: readonly string[], +): boolean { + if (!user) return false; + if (user.role === "admin") return true; + const held = new Set(user.permissions ?? []); + return keys.some((k) => held.has(k)); +} + +export function canAccessKeys( + user: PermissionHolder | null | undefined, + keys: PermissionKeys, +): boolean { + if (keys === "admin") return Boolean(user && user.role === "admin"); + return userCanAny(user, keys); +} + +export function navAllowed( + user: PermissionHolder | null | undefined, + navKey: NavPermissionKey, +): boolean { + return canAccessKeys(user, NAV_PERMISSIONS[navKey]); +} + +export function userCanKey( + user: PermissionHolder | null | undefined, + key: string | readonly string[], +): boolean { + return typeof key === "string" ? userCan(user, key) : userCanAny(user, key); +} + +/** + * Permissions that unlock a dashboard path (any-of). + * ``"admin"`` means role===admin only (no module key this round). + * ``null`` means no special gate. + */ +export function pathPermissionKeys(pathname: string): PermissionKeys | null { + if (pathname.startsWith("/admin/users") || pathname === "/admin/sso") { + return PERM.usersPage; + } + if (pathname.startsWith("/admin/models") || pathname === "/models") { + return PERM.modelsPage; + } + if (pathname.startsWith("/admin/backend")) { + return PERM.storage; + } + if ( + pathname.startsWith("/admin/plugins") || + pathname === "/plugins" || + pathname.startsWith("/plugins/") + ) { + return PERM.plugins; + } + if ( + pathname.startsWith("/admin/security") || + pathname.startsWith("/admin/audit") + ) { + return PERM.securityPage; + } + if ( + pathname.startsWith("/admin/advanced") || + pathname.startsWith("/admin/voice") || + pathname.startsWith("/admin/updates") + ) { + return PERM.advancedPage; + } + if (pathname.startsWith("/admin/")) { + return "admin"; + } + if ( + pathname === "/personalization/channels" || + pathname === "/channels" || + pathname.startsWith("/personalization/channels/") + ) { + return PERM.channels; + } + if (pathname === "/connectors" || pathname.startsWith("/connectors/")) { + return PERM.connectors; + } + if ( + pathname === "/skill-packages" || + pathname.startsWith("/skill-packages/") + ) { + return PERM.skillPackages; + } + if ( + pathname === "/knowledge-bases" || + pathname.startsWith("/knowledge-bases/") + ) { + return PERM.knowledgeBasesPage; + } + if ( + pathname === "/remote-desktop" || + pathname.startsWith("/remote-desktop/") + ) { + return PERM.desktop; + } + if (pathname === "/workbench/terminal" || pathname === "/terminal") { + return PERM.terminal; + } + if (pathname === "/workbench/browser" || pathname === "/remote-browser") { + return PERM.browser; + } + if (pathname === "/workbench" || pathname.startsWith("/workbench/")) { + return PERM.workbench; + } + if (pathname === "/acp" || pathname.startsWith("/acp/")) { + return "admin"; + } + return null; +} + +/** True when this route config path should be wrapped in RequirePermission. */ +export function routeNeedsPermission(routePath: string): boolean { + const probe = routePath.replace(/\/\*$/, "").replace(/\/:[^/]+/g, ""); + if (pathPermissionKeys(probe) !== null) return true; + if (probe === "/personalization" || probe.startsWith("/personalization/")) { + return true; + } + if (probe === "/workbench" || probe.startsWith("/workbench/")) return true; + if (probe.startsWith("/admin")) return true; + return false; +} + +export function canAccessPath( + user: PermissionHolder | null | undefined, + pathname: string, +): boolean { + const req = pathPermissionKeys(pathname); + if (req === null) return true; + return canAccessKeys(user, req); +} diff --git a/dashboard/src/utils/sharedExpert.ts b/dashboard/src/utils/sharedExpert.ts new file mode 100644 index 00000000..523f9cca --- /dev/null +++ b/dashboard/src/utils/sharedExpert.ts @@ -0,0 +1,18 @@ +export interface SharedExpertAccess { + is_shared?: boolean; + is_owner?: boolean; +} + +export function isSharedExpertViewer(agent: SharedExpertAccess): boolean { + return agent.is_shared === true && agent.is_owner === false; +} + +/** True when the current user may manage this expert (not a share-only viewer). */ +export function isOwnedExpert(agent: SharedExpertAccess): boolean { + return !isSharedExpertViewer(agent); +} + +/** Experts the user owns — for Experts / Personalization / agent bars. */ +export function ownedExperts(agents: T[]): T[] { + return agents.filter(isOwnedExpert); +} diff --git a/dashboard/src/utils/subagentEmojis.test.ts b/dashboard/src/utils/subagentEmojis.test.ts new file mode 100644 index 00000000..8acf1464 --- /dev/null +++ b/dashboard/src/utils/subagentEmojis.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { SUBAGENT_EMOJI_OPTIONS } from "./subagentEmojis"; + +describe("SUBAGENT_EMOJI_OPTIONS", () => { + it("starts with the default robot emoji", () => { + expect(SUBAGENT_EMOJI_OPTIONS[0]).toBe("🤖"); + }); + + it("has no duplicate entries", () => { + expect(new Set(SUBAGENT_EMOJI_OPTIONS).size).toBe( + SUBAGENT_EMOJI_OPTIONS.length, + ); + }); + + it("covers a full catalog-sized set for picker browsing", () => { + expect(SUBAGENT_EMOJI_OPTIONS.length).toBeGreaterThanOrEqual(100); + }); +}); diff --git a/dashboard/src/utils/subagentEmojis.ts b/dashboard/src/utils/subagentEmojis.ts new file mode 100644 index 00000000..9a1f6ed0 --- /dev/null +++ b/dashboard/src/utils/subagentEmojis.ts @@ -0,0 +1,124 @@ +// Auto-curated from bundled subagent catalog emojis (+ stable default first). +export const SUBAGENT_EMOJI_OPTIONS = [ + "🤖", + "⏱️", + "☁️", + "♟️", + "♿", + "⚔️", + "⚕️", + "⚖️", + "⚙️", + "⚡", + "⛓️", + "✅", + "✍️", + "✨", + "🇨🇳", + "🇫🇷", + "🇰🇷", + "🌈", + "🌍", + "🌏", + "🌐", + "🌟", + "🌱", + "🌸", + "🌿", + "🍎", + "🎓", + "🎙️", + "🎛️", + "🎠", + "🎥", + "🎧", + "🎨", + "🎬", + "🎭", + "🎮", + "🎯", + "🎵", + "🏋️", + "🏔️", + "🏗️", + "🏛️", + "🏠", + "🏢", + "🏥", + "🏦", + "🏨", + "🏭", + "🏹", + "🐑", + "🐦", + "👁️", + "💎", + "💬", + "💰", + "💸", + "💼", + "📄", + "📈", + "📊", + "📋", + "📐", + "📒", + "📖", + "📘", + "📚", + "📜", + "📝", + "📡", + "📣", + "📤", + "📦", + "📧", + "📱", + "📲", + "📷", + "📸", + "📺", + "🔄", + "🔌", + "🔍", + "🔎", + "🔐", + "🔒", + "🔗", + "🔥", + "🔧", + "🔩", + "🔬", + "🔭", + "🔮", + "🕸️", + "🕹️", + "🖥️", + "🖧", + "🗃️", + "🗄️", + "🗡️", + "🗣️", + "🗺️", + "🚀", + "🚨", + "🛍️", + "🛒", + "🛠️", + "🛡️", + "🛰️", + "🛸", + "🤝", + "🥽", + "🧐", + "🧠", + "🧪", + "🧬", + "🧭", + "🧱", + "🧲", + "🪡", + "🫧", +] as const; + +export type SubagentEmojiOption = (typeof SUBAGENT_EMOJI_OPTIONS)[number]; diff --git a/dashboard/src/utils/updateStatusCache.test.ts b/dashboard/src/utils/updateStatusCache.test.ts index e884ae25..71eb54a8 100644 --- a/dashboard/src/utils/updateStatusCache.test.ts +++ b/dashboard/src/utils/updateStatusCache.test.ts @@ -4,6 +4,7 @@ import type { UpdateStatus } from "../api/modules/update"; import { UPDATE_STATUS_STORAGE_KEY, UPDATE_STATUS_TTL_MS, + UPDATE_STATUS_CHANGED_EVENT, clearStoredUpdateStatus, isUpdateStatusCacheExpired, readStoredUpdateStatus, @@ -57,4 +58,18 @@ describe("updateStatusCache", () => { clearStoredUpdateStatus(); expect(localStorage.getItem(UPDATE_STATUS_STORAGE_KEY)).toBeNull(); }); + + it("dispatches change event when storing", () => { + const seen: UpdateStatus[] = []; + const handler = (event: Event) => { + seen.push((event as CustomEvent).detail); + }; + window.addEventListener(UPDATE_STATUS_CHANGED_EVENT, handler); + try { + storeUpdateStatus(sample); + expect(seen).toEqual([sample]); + } finally { + window.removeEventListener(UPDATE_STATUS_CHANGED_EVENT, handler); + } + }); }); diff --git a/dashboard/src/utils/updateStatusCache.ts b/dashboard/src/utils/updateStatusCache.ts index d8c15c98..2da60335 100644 --- a/dashboard/src/utils/updateStatusCache.ts +++ b/dashboard/src/utils/updateStatusCache.ts @@ -2,6 +2,9 @@ import type { UpdateStatus } from "../api/modules/update"; export const UPDATE_STATUS_STORAGE_KEY = "octop:update-status"; export const UPDATE_STATUS_TTL_MS = 60 * 60 * 1000; +/** How often the UI re-evaluates the local cache / probes the API. */ +export const UPDATE_STATUS_POLL_MS = UPDATE_STATUS_TTL_MS; +export const UPDATE_STATUS_CHANGED_EVENT = "octop:update-status-changed"; interface StoredUpdateStatus { checkedAt: number; @@ -40,6 +43,11 @@ export function storeUpdateStatus( } catch { // quota / private mode } + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent(UPDATE_STATUS_CHANGED_EVENT, { detail: status }), + ); + } } export function clearStoredUpdateStatus(): void { diff --git a/docs/api.md b/docs/api.md index 413b3693..0b04985b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -30,8 +30,9 @@ admin can clear the lockout with `POST /api/users/{id}/unlock-login`. ### Public endpoints (no token) `/api/docs`, `/api/openapi.json`, `/api/health`, `/api/setup/*`, -`/api/auth/login`, `/api/connectors/oauth/callback`, and -`/api/internal/mcp/*`. All other routes are JWT-gated by +`/api/auth/login`, `/api/auth/oidc/status`, `/api/auth/oidc/start`, +`/api/auth/oidc/callback`, `/api/auth/oidc/exchange`, +`/api/connectors/oauth/callback`, and `/api/internal/mcp/*`. All other routes are JWT-gated by `api/middleware/jwt_auth.py`; the setup lockdown middleware (`api/middleware/setup_lockdown.py`) additionally blocks non-setup routes until the wizard finishes. @@ -50,6 +51,13 @@ routes until the wizard finishes. | `POST` | `/setup/test-provider` | public | Ping a provider draft (kind/base_url/api_key/model) | | `POST` | `/setup/finish` | public | Finalise setup and unlock the rest of the API | | `POST` | `/auth/login` | public | body `{username, password}` → `{access_token, role, user, ...}` | +| `GET` | `/auth/oidc/status` | public | OIDC login availability and provider display name | +| `POST` | `/auth/oidc/start` | public | body `{redirect_after?}` → identity-provider authorization URL | +| `GET` | `/auth/oidc/callback` | public | Identity-provider callback; redirects to dashboard login completion | +| `POST` | `/auth/oidc/exchange` | public | body `{code}` → same JWT response as `/auth/login` | +| `GET` | `/auth/oidc/config` | admin | OIDC provider configuration and callback URL; client secret is omitted | +| `PUT` | `/auth/oidc/config` | admin | Write OIDC provider configuration; `client_secret` is write-only | +| `POST` | `/auth/oidc/config/test` | admin | Verify configured discovery metadata and JWKS endpoint | | `POST` | `/auth/logout` | user | `204` | | `GET` | `/auth/me` | user | `{id, username, role, display_name, locale, ...}` | | `PATCH` | `/auth/me` | user | body `{display_name?, locale?, ...}` | diff --git a/docs/cli.md b/docs/cli.md index 8c805bfd..02e1dcbd 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -318,20 +318,40 @@ Commands: ## `octop backup` -Export and restore Octop backups. Works fully offline — the wheel -includes a `octop-backup` entrypoint that the dashboard can also -invoke. +Export and restore Octop backups. Works fully offline. ``` Usage: octop backup [OPTIONS] COMMAND [ARGS]... - Export and restore Octop backups. + Backup and restore database + local agent workspaces. Commands: create Create a backup archive (DB + workspaces + config). restore Restore a backup archive into ~/.octop. + auto Automatic backup status and one-shot run. ``` +### `octop backup auto` + +Automatic backups are scheduled **inside** a running `octop run` process +(via the same system-job mechanism as TLS renewal). Configure in +`config.json` under `backup`, with env overrides +`OCTOP_BACKUP_AUTO_ENABLED`, `OCTOP_BACKUP_SCHEDULE`, +`OCTOP_BACKUP_RETENTION_COUNT`, or via the dashboard Backup settings. + +``` +Usage: octop backup auto [OPTIONS] COMMAND [ARGS]... + +Commands: + status Print automatic backup settings from config.json. + run Create one automatic backup now and prune by retention. +``` + +Default schedule when enabled: `cron:0 4 * * *` (daily 04:00, server +timezone). Automatic archives are named `octop-auto-backup-*.tar.gz`; +retention only deletes those files and never touches manual +`octop-backup-*.tar.gz` archives. + ## `octop update` Check for and install a newer Octop release from the configured diff --git a/pyproject.toml b/pyproject.toml index e1d46b6a..11d02003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "octop" -version = "0.9.23" +version = "0.9.24" description = "Smarter self-hosted AI assistant for multiple users and agents, built on harness-agent" readme = "README.md" license = { text = "MIT" } @@ -32,13 +32,15 @@ dependencies = [ "acme>=5.6.0", "josepy>=2.2.0", "boto3>=1.40.61", - "harness-browser>=0.7.4", + "harness-browser>=0.7.5", "playwright>=1.40", "psycopg[binary]>=3.2", "langgraph-checkpoint-postgres>=2.0", "mcp>=1.9,<2", "pillow>=10.0", + "pypdf>=5.0", "python-docx>=1.2.0", + "python-pptx>=1.0", ] [project.optional-dependencies] @@ -48,6 +50,7 @@ dev = [ "pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=4.1", + "pytest-testmon>=2.0", "httpx>=0.27", "ruff>=0.4", "mypy>=1.10", @@ -59,6 +62,8 @@ browser = [ ] # Kept for backward compatibility; pillow is now a core dependency. desktop = ["mss>=9.0", "pynput>=1.7"] +# Local ONNX / fastembed embedding models (Models admin local tab). +local-embedding = ["fastembed>=0.4", "huggingface_hub>=0.20"] [dependency-groups] # PEP 735 dev group — installed by default on `uv sync`. Mirrors the @@ -67,6 +72,7 @@ dev = [ "pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=4.1", + "pytest-testmon>=2.0", "httpx>=0.27", "ruff>=0.4", "mypy>=1.10", diff --git a/src/octop/__init__.py b/src/octop/__init__.py index a1da1a61..ed012837 100644 --- a/src/octop/__init__.py +++ b/src/octop/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.9.23" +__version__ = "0.9.24" diff --git a/src/octop/api/app.py b/src/octop/api/app.py index a6a4010a..14ba5e8f 100644 --- a/src/octop/api/app.py +++ b/src/octop/api/app.py @@ -127,6 +127,7 @@ async def acme_http01_challenge(token: str) -> PlainTextResponse: agent_files, agents, auth, + auth_oidc, backup, browser, channels, @@ -139,10 +140,12 @@ async def acme_http01_challenge(token: str) -> PlainTextResponse: health, i18n, internal_mcp, + knowledge_bases, mbti, memory, memory_portable, ollama_models, + onnx_models, plugins, preferences, proactive_care, @@ -176,6 +179,7 @@ async def acme_http01_challenge(token: str) -> PlainTextResponse: [ _RouterMount(setup.router, "/api", ["setup"]), _RouterMount(auth.router, "/api/auth", ["auth"]), + _RouterMount(auth_oidc.router, "/api/auth", ["auth"]), _RouterMount(preferences.router, "/api", ["auth"]), _RouterMount(i18n.router, "/api", ["i18n"]), _RouterMount(health.router, "/api/health", ["health"]), @@ -185,6 +189,7 @@ async def acme_http01_challenge(token: str) -> PlainTextResponse: _RouterMount(chat.router, "/api", ["chat"]), _RouterMount(slash.router, "/api", ["slash"]), _RouterMount(connectors.router, "/api", ["connectors"]), + _RouterMount(knowledge_bases.router, "/api", ["knowledge"]), _RouterMount(internal_mcp.router, "/api", ["internal-mcp"]), _RouterMount(channels.router, "/api", ["channels"]), _RouterMount(cron.router, "/api", ["cron"]), @@ -223,6 +228,7 @@ async def acme_http01_challenge(token: str) -> PlainTextResponse: _RouterMount(browser.router, "/api", ["browser"]), _RouterMount(desktop.router, "/api", ["desktop"]), _RouterMount(ollama_models.router, "/api", ["ollama"]), + _RouterMount(onnx_models.router, "/api", ["onnx"]), _RouterMount(plugins.router, "/api", ["plugins"]), ], ) diff --git a/src/octop/api/common/agent.py b/src/octop/api/common/agent.py index 941de6b9..98a20e85 100644 --- a/src/octop/api/common/agent.py +++ b/src/octop/api/common/agent.py @@ -7,14 +7,35 @@ from octop.infra.errors import ErrorCode, OctopError +def agent_is_shared(row: Any) -> bool: + return int(getattr(row, "is_shared", 0) or 0) == 1 + + +def user_owns_agent(row: Any, user: Any) -> bool: + return row.user_id is not None and row.user_id == user.id + + def assert_agent_owner(row: Any, user: Any) -> None: - """Raise if the user may not access this agent row (admin bypasses).""" + """Raise if the user may not mutate this agent row (admin bypasses).""" if user.is_admin: return if row.user_id is None or row.user_id != user.id: raise OctopError(ErrorCode.FORBIDDEN, "agent not owned by user") +def _user_may_access(row: Any, user: Any) -> bool: + if user.is_admin: + return True + if row.user_id is not None and row.user_id == user.id: + return True + return bool(agent_is_shared(row)) + + +def assert_agent_access_row(row: Any, user: Any) -> None: + if not _user_may_access(row, user): + raise OctopError(ErrorCode.FORBIDDEN, "agent not accessible to user") + + def require_agent_row( agent_id: str, *, @@ -36,7 +57,20 @@ def require_agent_row( if row.user_id is not None and row.user_id != as_user: raise OctopError(ErrorCode.FORBIDDEN, "agent not owned by as_user") else: - assert_agent_owner(row, user) + assert_agent_access_row(row, user) + return row + + +def require_agent_owner_row( + agent_id: str, + *, + user: Any, + as_user: int | None, + server: Any, +) -> Any: + """Load an agent row and require owner-level access.""" + row = require_agent_row(agent_id, user=user, as_user=as_user, server=server) + assert_agent_owner(row, user) return row diff --git a/src/octop/api/common/memory_client.py b/src/octop/api/common/memory_client.py index 958642bf..12fdec8f 100644 --- a/src/octop/api/common/memory_client.py +++ b/src/octop/api/common/memory_client.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Any -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.infra.agents.memory_backend import open_memory_kwargs from octop.infra.errors import ErrorCode, OctopError @@ -131,7 +131,7 @@ def call_memory_rpc( as_user: int | None, server: Any, ) -> Any: - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) _memory, bridge = _open_memory_for_agent(server, agent_id) payload = { "jsonrpc": "2.0", diff --git a/src/octop/api/common/public_base.py b/src/octop/api/common/public_base.py new file mode 100644 index 00000000..79bd024b --- /dev/null +++ b/src/octop/api/common/public_base.py @@ -0,0 +1,29 @@ +"""Resolve the externally visible HTTP origin for an incoming request.""" + +from __future__ import annotations + +from starlette.requests import Request + + +def _first_header_value(value: str | None) -> str | None: + if value is None: + return None + first = value.split(",", 1)[0].strip() + return first or None + + +def resolve_public_base(request: Request) -> str: + """Resolve the externally visible origin for an incoming request. + + Trusts ``X-Forwarded-Proto`` / ``X-Forwarded-Host`` when present (typical + behind a reverse proxy). Prefer deploying Octop behind a trusted proxy and + keeping the dashboard same-origin with the API for OIDC cookies. + """ + forwarded_proto = _first_header_value(request.headers.get("x-forwarded-proto")) + forwarded_host = _first_header_value(request.headers.get("x-forwarded-host")) + if forwarded_proto: + host = forwarded_host or request.headers.get("host") + if host: + return f"{forwarded_proto}://{host}".rstrip("/") + + return str(request.url.replace(path="", query="", fragment="")).rstrip("/") diff --git a/src/octop/api/common/workspace.py b/src/octop/api/common/workspace.py index 7805bd9b..16149c71 100644 --- a/src/octop/api/common/workspace.py +++ b/src/octop/api/common/workspace.py @@ -6,7 +6,7 @@ from harness_agent.backends.workspace import BackendWorkspace -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row, require_agent_row if TYPE_CHECKING: from harness_agent import HarnessAgent @@ -39,9 +39,11 @@ async def require_running_workspace( user: Any, as_user: int | None, server: Any, + owner_only: bool = False, ) -> BackendWorkspace: """Auth-checked :class:`BackendWorkspace` for a running agent.""" - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + checker = require_agent_owner_row if owner_only else require_agent_row + checker(agent_id, user=user, as_user=as_user, server=server) return require_running_agent(server, agent_id).workspace diff --git a/src/octop/api/deps.py b/src/octop/api/deps.py index a57eb122..b641673e 100644 --- a/src/octop/api/deps.py +++ b/src/octop/api/deps.py @@ -3,16 +3,20 @@ from __future__ import annotations import time +from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Any, cast import jwt from fastapi import Depends, Header, Query, Request from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.permissions import PERMISSIONS, user_has_permission if TYPE_CHECKING: from octop.infra.server import OctopServer from octop.infra.users.identity import User +else: + from octop.infra.users.identity import User class InvalidToken(Exception): ... @@ -69,6 +73,10 @@ def decode_token(secret: bytes, token: str) -> dict[str, Any]: _JWT_EXEMPT_EXACT = ( "/api/health", "/api/auth/login", + "/api/auth/oidc/status", + "/api/auth/oidc/start", + "/api/auth/oidc/callback", + "/api/auth/oidc/exchange", "/api/docs", "/api/openapi.json", ) @@ -183,7 +191,33 @@ async def current_user( return resolve_user_from_token(server, raw) -async def current_admin(user: User = Depends(current_user)) -> User: - if not user.is_admin: - raise OctopError(ErrorCode.FORBIDDEN, "admin required") - return user +def require_permission(key: str) -> Callable[..., Awaitable[User]]: + """Dependency factory: require the module permission ``key``. + + ``admin`` is bypassed inside ``user_has_permission``. Unknown keys fail + fast at construction so typos cannot silently grant access. + """ + if key not in PERMISSIONS: + raise RuntimeError(f"unknown permission key: {key}") + + async def _dep(user: User = Depends(current_user)) -> User: + if not user_has_permission(user, key): + raise OctopError( + ErrorCode.FORBIDDEN, + "permission required", + details={"permission": key}, + ) + return user + + return _dep + + +def require_admin() -> Callable[..., Awaitable[User]]: + """Dependency factory: require the admin role (no module key).""" + + async def _dep(user: User = Depends(current_user)) -> User: + if not user.is_admin: + raise OctopError(ErrorCode.FORBIDDEN, "admin required") + return user + + return _dep diff --git a/src/octop/api/openapi_meta.py b/src/octop/api/openapi_meta.py index 859cf8f4..e92a8417 100644 --- a/src/octop/api/openapi_meta.py +++ b/src/octop/api/openapi_meta.py @@ -17,7 +17,8 @@ Most endpoints require a JWT bearer token: 1. Complete initial setup via `/api/setup/*` (first install only). -2. `POST /api/auth/login` with `username` and `password`. +2. `POST /api/auth/login` with `username` and `password`, or complete the OIDC flow + with `/api/auth/oidc/start` and `/api/auth/oidc/exchange` when SSO is enabled. 3. Send `Authorization: Bearer ` on subsequent requests. Access tokens use sliding renewal: when less than one-third of @@ -26,8 +27,9 @@ token when present. Public endpoints (no token): `/api/docs`, `/api/openapi.json`, `/api/health`, -`/api/setup/*`, `/api/auth/login`, `/api/connectors/oauth/callback`, and -`/api/internal/mcp/*`. +`/api/setup/*`, `/api/auth/login`, `/api/auth/oidc/status`, `/api/auth/oidc/start`, +`/api/auth/oidc/callback`, `/api/auth/oidc/exchange`, `/api/connectors/oauth/callback`, +and `/api/internal/mcp/*`. ## Agent scope @@ -68,6 +70,10 @@ "name": "connectors", "description": "Third-party integrations (Notion, Figma, …) exposed as MCP servers.", }, + { + "name": "knowledge", + "description": "Private, shareable document knowledge bases and their indexing capability.", + }, { "name": "internal-mcp", "description": "Internal MCP gateway used by harness agents (no dashboard auth).", @@ -134,6 +140,10 @@ "description": "Remote OS desktop stream and input injection (admin only).", }, {"name": "ollama", "description": "Local Ollama model discovery and download management."}, + { + "name": "onnx", + "description": "Local ONNX / fastembed embedding service: enable, catalog, and download.", + }, ] _BEARER_SCHEME = { diff --git a/src/octop/api/routers/acp.py b/src/octop/api/routers/acp.py index 997db298..a99320a7 100644 --- a/src/octop/api/routers/acp.py +++ b/src/octop/api/routers/acp.py @@ -14,7 +14,7 @@ from pydantic import BaseModel, Field from octop.api.common.agent import assert_agent_owner as _assert_agent_owner -from octop.api.deps import current_user, get_server +from octop.api.deps import current_user, get_server, require_admin from octop.infra.errors import ErrorCode, OctopError logger = logging.getLogger(__name__) @@ -145,7 +145,7 @@ def _agent_row(server: Any, agent_id: str, user: Any) -> Any: @router.get("/acp", summary="Get global ACP runners") async def get_global_acp_runners( - user: Any = Depends(current_user), + user: Any = Depends(require_admin()), server: Any = Depends(get_server), ) -> dict[str, Any]: """Return ACP runner definitions shared by all agents for the current user.""" @@ -156,7 +156,7 @@ async def get_global_acp_runners( @router.put("/acp", summary="Update global ACP runners") async def put_global_acp_runners( body: ACPRunnersBody, - user: Any = Depends(current_user), + user: Any = Depends(require_admin()), server: Any = Depends(get_server), ) -> dict[str, Any]: """Replace the current user's global ACP runner configuration.""" @@ -170,7 +170,7 @@ async def put_global_acp_runners( @router.get("/acp/{runner_name}", summary="Get global ACP runner") async def get_global_acp_runner( runner_name: str, - user: Any = Depends(current_user), + user: Any = Depends(require_admin()), server: Any = Depends(get_server), ) -> dict[str, Any]: registry = _registry(server) @@ -184,7 +184,7 @@ async def get_global_acp_runner( async def put_global_acp_runner( runner_name: str, body: ACPRunnerBody = Body(...), - user: Any = Depends(current_user), + user: Any = Depends(require_admin()), server: Any = Depends(get_server), ) -> dict[str, Any]: key = runner_name.strip() @@ -202,7 +202,7 @@ async def put_global_acp_runner( @router.delete("/acp/{runner_name}", status_code=204, summary="Delete global ACP runner") async def delete_global_acp_runner( runner_name: str, - user: Any = Depends(current_user), + user: Any = Depends(require_admin()), server: Any = Depends(get_server), ) -> None: if runner_name in _BUILTIN_RUNNERS: diff --git a/src/octop/api/routers/admin.py b/src/octop/api/routers/admin.py index 0a867168..1206f224 100644 --- a/src/octop/api/routers/admin.py +++ b/src/octop/api/routers/admin.py @@ -6,14 +6,14 @@ from fastapi import APIRouter, Depends -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission router = APIRouter() @router.get("/overview") async def overview( - _: Any = Depends(current_admin), server: Any = Depends(get_server) + _: Any = Depends(require_permission("admin_console")), server: Any = Depends(get_server) ) -> dict[str, Any]: assert server.app_runtime is not None users = [] @@ -50,7 +50,7 @@ async def audit_log( actor: str | None = None, action: str | None = None, limit: int = 100, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("admin_console")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: rows = server.services.audit_repo.query(since=since, actor=actor, action=action, limit=limit) @@ -69,7 +69,7 @@ async def audit_log( @router.get("/metrics") async def metrics( - _: Any = Depends(current_admin), server: Any = Depends(get_server) + _: Any = Depends(require_permission("admin_console")), server: Any = Depends(get_server) ) -> dict[str, Any]: from octop.infra.metrics import METRICS diff --git a/src/octop/api/routers/agent_files.py b/src/octop/api/routers/agent_files.py index 1acc21ca..376b9397 100644 --- a/src/octop/api/routers/agent_files.py +++ b/src/octop/api/routers/agent_files.py @@ -40,7 +40,7 @@ from fastapi import APIRouter, Depends, Response from pydantic import BaseModel -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.common.agent_workspace import resolve_agent_workspace_dir from octop.api.common.workspace import require_running_workspace from octop.api.deps import current_user, get_server @@ -56,7 +56,7 @@ def _resolve_runtime(agent_id: str, *, user: Any, as_user: int | None, server: Any) -> Any: """Return the AgentRow for the given agent_id after auth.""" - return require_agent_row(agent_id, user=user, as_user=as_user, server=server) + return require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) # --- heartbeat config ------------------------------------------------------ diff --git a/src/octop/api/routers/agents.py b/src/octop/api/routers/agents.py index 78bb2b17..61a9f167 100644 --- a/src/octop/api/routers/agents.py +++ b/src/octop/api/routers/agents.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, Query -from octop.api.common.agent import assert_agent_owner +from octop.api.common.agent import assert_agent_access_row, assert_agent_owner from octop.api.common.agent_runtime import AgentRuntimeFields, runtime_field_updates from octop.api.deps import current_user, get_server from octop.infra.agents.runtime_limits import ( @@ -16,6 +16,7 @@ agent_runtime_values, ) from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.permissions import user_has_permission logger = logging.getLogger(__name__) @@ -31,6 +32,7 @@ class AgentCreateBody(AgentRuntimeFields): config: dict[str, Any] = {} icon: str | None = None template_name: str | None = None + is_shared: bool = False class AgentPatchBody(AgentRuntimeFields): @@ -42,6 +44,7 @@ class AgentPatchBody(AgentRuntimeFields): config: dict[str, Any] | None = None icon: str | None = None template_name: str | None = None + is_shared: bool | None = None def _attach_unread_counts( @@ -58,9 +61,17 @@ def _bootstrap_pending_for(server: Any, agent_id: str) -> bool: return not server.app_runtime.agent_registry.is_bootstrapped(agent_id) +def _owner_username(server: Any, row: Any) -> str | None: + if row.user_id is None: + return None + owner = server.services.user_repo.get(row.user_id) + return owner.username if owner is not None else None + + def _row_dict( row: Any, *, + viewer_user_id: int | None = None, owner_username: str | None = None, bootstrap_pending: bool | None = None, ) -> dict[str, Any]: @@ -87,10 +98,11 @@ def _row_dict( "template_name": row.template_name, "icon_name": cfg.get("icon_name"), "color": cfg.get("color"), + "is_shared": bool(int(getattr(row, "is_shared", 0) or 0)), + "is_owner": row.user_id is not None and row.user_id == viewer_user_id, + "owner_username": owner_username, **agent_runtime_values(cfg), } - if owner_username is not None: - payload["owner_username"] = owner_username if bootstrap_pending is not None: payload["bootstrap_pending"] = bootstrap_pending return payload @@ -107,12 +119,16 @@ async def list_agents( ) -> list[dict[str, Any]]: """List agents for the dashboard. - Default ``scope=mine`` returns only agents owned by the authenticated user - (including admins — chat/experts sidebars must not leak other users' agents). - Admins may pass ``scope=all`` for the admin agents overview. + Default ``scope=mine`` returns agents owned by the authenticated user plus + agents other users have explicitly shared. + Holders of the ``users`` permission (and admins) may pass ``scope=all``. """ - if scope == "all" and not user.is_admin: - raise OctopError(ErrorCode.FORBIDDEN, "scope=all requires admin") + if scope == "all" and not user_has_permission(user, "users"): + raise OctopError( + ErrorCode.FORBIDDEN, + "permission required", + details={"permission": "users"}, + ) assert server.app_runtime is not None registry = server.app_runtime.agent_registry @@ -127,6 +143,7 @@ async def list_agents( payloads = [ _row_dict( r, + viewer_user_id=user.id, owner_username=username_by_id.get(r.user_id) if r.user_id is not None else None, bootstrap_pending=_bootstrap_pending_for(server, r.agent_id), ) @@ -134,13 +151,26 @@ async def list_agents( ] return _attach_unread_counts(server, user.id, payloads) - rows = registry.list_agents(user.id) + owned = registry.list_agents(user.id) + shared = server.services.agent_repo.list_shared(exclude_user_id=user.id) + rows = list({row.agent_id: row for row in [*shared, *owned]}.values()) + shared_owner_username_by_id: dict[int, str] = {} + for row in shared: + if row.user_id is None or row.user_id in shared_owner_username_by_id: + continue + owner = server.services.user_repo.get(row.user_id) + if owner is not None: + shared_owner_username_by_id[row.user_id] = owner.username return _attach_unread_counts( server, user.id, [ _row_dict( r, + viewer_user_id=user.id, + owner_username=( + shared_owner_username_by_id.get(r.user_id) if r.user_id is not None else None + ), bootstrap_pending=_bootstrap_pending_for(server, r.agent_id), ) for r in rows @@ -169,9 +199,15 @@ async def create_agent( runtime_config=runtime_field_updates(body, exclude_unset=True), icon=body.icon, template_name=body.template_name, + is_shared=body.is_shared, ) row = await server.app_runtime.agent_registry.create(spec) - return _row_dict(row, bootstrap_pending=_bootstrap_pending_for(server, row.agent_id)) + return _row_dict( + row, + viewer_user_id=user.id, + owner_username=user.username, + bootstrap_pending=_bootstrap_pending_for(server, row.agent_id), + ) @router.post("/{agent_id}/read", status_code=204, summary="Mark agent read") @@ -200,9 +236,11 @@ async def get_agent( row = server.app_runtime.agent_registry.get_row(agent_id) if row is None: raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {agent_id!r} not found") - _assert_agent_owner(row, user) + assert_agent_access_row(row, user) return _row_dict( row, + viewer_user_id=user.id, + owner_username=_owner_username(server, row), bootstrap_pending=_bootstrap_pending_for(server, agent_id), ) @@ -223,12 +261,23 @@ async def patch_agent( if row is None: raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {agent_id!r} not found") _assert_agent_owner(row, user) - row = await server.app_runtime.agent_registry.update( - agent_id, - **{k: v for k, v in body.model_dump(exclude_unset=True).items() if k != "config"}, - **({"config_json": json.dumps(body.config)} if body.config is not None else {}), + updates = { + key: value + for key, value in body.model_dump(exclude_unset=True).items() + if key not in {"config", "is_shared"} + } + if body.config is not None: + updates["config_json"] = json.dumps(body.config) + if updates: + row = await server.app_runtime.agent_registry.update(agent_id, **updates) + if body.is_shared is not None: + row = await server.app_runtime.agent_registry.set_shared(agent_id, body.is_shared) + return _row_dict( + row, + viewer_user_id=user.id, + owner_username=user.username, + bootstrap_pending=_bootstrap_pending_for(server, agent_id), ) - return _row_dict(row, bootstrap_pending=_bootstrap_pending_for(server, agent_id)) @router.delete("/{agent_id}", status_code=204, summary="Delete agent") diff --git a/src/octop/api/routers/auth.py b/src/octop/api/routers/auth.py index 199ad0ec..ad55910a 100644 --- a/src/octop/api/routers/auth.py +++ b/src/octop/api/routers/auth.py @@ -9,6 +9,7 @@ from octop.api.deps import current_user, get_server, sign_token from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.permissions import effective_permissions from octop.infra.utils.locale import normalize_locale router = APIRouter() @@ -22,6 +23,7 @@ def _user_json(user: Any, *, locale: str | None = None) -> dict[str, Any]: "role": user.role.value, "display_name": user.display_name, "locale": loc, + "permissions": effective_permissions(user), } diff --git a/src/octop/api/routers/auth_oidc.py b/src/octop/api/routers/auth_oidc.py new file mode 100644 index 00000000..25af5b7a --- /dev/null +++ b/src/octop/api/routers/auth_oidc.py @@ -0,0 +1,192 @@ +"""OpenID Connect SSO HTTP routes.""" + +from __future__ import annotations + +import asyncio +import secrets +from functools import partial +from typing import Any, cast + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse, RedirectResponse +from pydantic import BaseModel, Field + +from octop.api.common.public_base import resolve_public_base +from octop.api.deps import get_server, require_permission, sign_token +from octop.api.routers.auth import _user_json +from octop.infra.auth.sso.service import SsoService +from octop.infra.errors import ErrorCode, OctopError + +router = APIRouter() +_OIDC_STATE_COOKIE = "octop_oidc_state" +_OIDC_COOKIE_PATH = "/api/auth/oidc" +_OIDC_STATE_TTL_SECONDS = 600 + + +class OidcStartBody(BaseModel): + redirect_after: str | None = None + + +class OidcExchangeBody(BaseModel): + code: str = Field(min_length=1) + + +class OidcConfigBody(BaseModel): + enabled: bool | None = None + display_name: str | None = None + issuer: str | None = None + client_id: str | None = None + client_secret: str | None = None + scopes: str | None = None + dashboard_origin: str | None = None + + +def _service(server: Any) -> SsoService: + return cast(SsoService, server.sso_service) + + +def _public_base(request: Request) -> str: + return resolve_public_base(request) + + +def _request_is_https(request: Request) -> bool: + forwarded_proto = request.headers.get("x-forwarded-proto") + if forwarded_proto: + return forwarded_proto.split(",", 1)[0].strip().lower() == "https" + return request.url.scheme == "https" + + +def _bad_request(exc: ValueError) -> OctopError: + return OctopError.localized(ErrorCode.OIDC_BAD_REQUEST, detail=str(exc)) + + +def _login_error_redirect(server: Any, public_base: str) -> str: + """Prefer configured dashboard_origin for OIDC error pages.""" + return _service(server).login_error_frontend(public_base) + + +@router.get("/oidc/status", summary="OIDC login button status") +async def oidc_status(server: Any = Depends(get_server)) -> dict[str, bool | str]: + """Return whether OIDC login is available and the configured provider label.""" + return _service(server).status() + + +@router.post("/oidc/start", summary="Begin OIDC login") +async def oidc_start( + body: OidcStartBody, + request: Request, + server: Any = Depends(get_server), +) -> JSONResponse: + """Create OIDC state and return the identity provider authorization URL.""" + service = _service(server) + try: + started = await asyncio.get_running_loop().run_in_executor( + None, + partial( + service.start_login, + redirect_after=body.redirect_after, + public_base=_public_base(request), + ), + ) + state = started.get("state") + if not isinstance(state, str) or not state: + raise ValueError("OIDC authorization state is missing") + except ValueError as exc: + raise _bad_request(exc) from exc + response = JSONResponse(started) + response.set_cookie( + _OIDC_STATE_COOKIE, + state, + max_age=_OIDC_STATE_TTL_SECONDS, + path=_OIDC_COOKIE_PATH, + httponly=True, + secure=_request_is_https(request), + samesite="lax", + ) + return response + + +@router.get("/oidc/callback", summary="OIDC identity provider callback") +async def oidc_callback( + request: Request, + code: str | None = None, + state: str | None = None, + error: str | None = None, + server: Any = Depends(get_server), +) -> RedirectResponse: + """Complete an OIDC authorization-code callback and redirect to the dashboard.""" + public_base = _public_base(request) + cookie_state = request.cookies.get(_OIDC_STATE_COOKIE) + if not state or not cookie_state or not secrets.compare_digest(cookie_state, state): + frontend = _login_error_redirect(server, public_base) + response = RedirectResponse(f"{frontend}/login?oidc_error=state", status_code=302) + else: + result = await _service(server).handle_callback( + code=code, + state=state, + error=error, + public_base=public_base, + ) + response = RedirectResponse(result.url, status_code=302) + response.delete_cookie( + _OIDC_STATE_COOKIE, path=_OIDC_COOKIE_PATH, secure=_request_is_https(request) + ) + return response + + +@router.post("/oidc/exchange", summary="Exchange OIDC login code") +async def oidc_exchange( + body: OidcExchangeBody, server: Any = Depends(get_server) +) -> dict[str, Any]: + """Exchange a short-lived browser login code for the standard JWT login response.""" + try: + user = await _service(server).exchange_login_code(body.code) + except ValueError as exc: + raise _bad_request(exc) from exc + secret = server.services.secret_repo.get("jwt") + ttl = server.services.config.access_token_ttl_seconds + return { + "access_token": sign_token( + secret, sub=user.id, uname=user.username, role=user.role.value, ttl_seconds=ttl + ), + "token_type": "Bearer", + "expires_in": ttl, + "user": _user_json(user, locale=user.locale), + } + + +@router.get("/oidc/config", summary="Get OIDC provider configuration") +async def oidc_config( + request: Request, + _: Any = Depends(require_permission("sso")), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Return the admin-safe OIDC provider configuration, excluding its client secret.""" + return _service(server).get_config_for_admin(public_base=_public_base(request)) + + +@router.put("/oidc/config", summary="Upsert OIDC provider configuration") +async def put_oidc_config( + body: OidcConfigBody, + request: Request, + _: Any = Depends(require_permission("sso")), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Create or update OIDC provider settings; the client secret is write-only.""" + return _service(server).put_config( + body.model_dump(exclude_unset=True), + public_base=_public_base(request), + ) + + +@router.post("/oidc/config/test", summary="Test OIDC discovery") +async def test_oidc_config( + _: Any = Depends(require_permission("sso")), server: Any = Depends(get_server) +) -> dict[str, bool | str]: + """Fetch OIDC discovery metadata and JWKS without changing configuration.""" + try: + return await asyncio.get_running_loop().run_in_executor( + None, _service(server).test_connection + ) + except ValueError as exc: + raise _bad_request(exc) from exc diff --git a/src/octop/api/routers/backup.py b/src/octop/api/routers/backup.py index 48612400..b55e07d8 100644 --- a/src/octop/api/routers/backup.py +++ b/src/octop/api/routers/backup.py @@ -7,9 +7,18 @@ from fastapi import APIRouter, Depends, File, Query, UploadFile from fastapi.responses import StreamingResponse +from pydantic import BaseModel, Field from octop.api.common.content_disposition import content_disposition -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission +from octop.config import load_config +from octop.infra.backup.auto import ( + AUTO_BACKUP_JOB_ID, + BACKUP_LOCK, + backup_config_from_payload, + run_auto_backup, + update_server_backup_config, +) from octop.infra.backup.store import ( delete_backup_file, list_backup_files, @@ -28,11 +37,32 @@ _MAX_IMPORT_BYTES = 512 * 1024 * 1024 +class AutoBackupSettingsBody(BaseModel): + auto_enabled: bool = False + schedule: str = Field(default="cron:0 4 * * *", min_length=1) + retention_count: int = Field(default=7, ge=1, le=365) + + def _agent_rows(server: Any) -> list[Any]: assert server.app_runtime is not None return cast(list[Any], server.app_runtime.agent_registry.list_rows()) +def _auto_settings_payload(server: Any) -> dict[str, Any]: + config = load_config(server.paths.config) + backup = config.backup + scheduled = False + runtime = getattr(server, "app_runtime", None) + if runtime is not None and runtime.cron_manager is not None: + scheduled = runtime.cron_manager.has_system_job(AUTO_BACKUP_JOB_ID) + return { + "auto_enabled": backup.auto_enabled, + "schedule": backup.schedule, + "retention_count": backup.retention_count, + "scheduled": scheduled, + } + + async def _rehydrate_runtime_after_restore(server: Any) -> None: """Sync restored providers/agents, IM channels, and cron (no process restart).""" runtime = getattr(server, "app_runtime", None) @@ -60,7 +90,7 @@ async def _rehydrate_runtime_after_restore(server: Any) -> None: @router.get("/backup/list", summary="List stored backup archives") async def list_backups( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> dict[str, Any]: """List ``.tar.gz`` files in ``~/.octop/backups/``.""" @@ -71,20 +101,67 @@ async def list_backups( } +@router.get("/backup/auto", summary="Get automatic backup settings") +async def get_auto_backup_settings( + _: Any = Depends(require_permission("backup")), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Return auto-backup config and whether the system job is currently scheduled.""" + return _auto_settings_payload(server) + + +@router.put("/backup/auto", summary="Update automatic backup settings") +async def put_auto_backup_settings( + body: AutoBackupSettingsBody, + _: Any = Depends(require_permission("backup")), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Persist auto-backup settings and reschedule the in-process system job.""" + backup = backup_config_from_payload(body.model_dump()) + update_server_backup_config(server, backup) + return {"ok": True, **_auto_settings_payload(server)} + + +@router.post("/backup/auto/run", summary="Run automatic backup now") +async def run_auto_backup_now( + _: Any = Depends(require_permission("backup")), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Create one automatic backup immediately (same path as the scheduled job).""" + if BACKUP_LOCK.locked(): + raise OctopError( + ErrorCode.BACKUP_IN_PROGRESS, + "a backup is already in progress", + ) + entry = await run_auto_backup(server) + if entry is None: + raise OctopError( + ErrorCode.BACKUP_IN_PROGRESS, + "auto backup skipped (busy or runtime not ready)", + ) + return {"ok": True, "item": entry.to_dict()} + + @router.post("/backup/create", summary="Create backup and save to backups dir") async def create_backup( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Create a full backup archive and persist it under ``backups_dir``.""" assert server.services is not None - data, filename = create_system_backup( - paths=server.paths, - agent_rows=_agent_rows(server), - pool=server.services.db, - db_config=server.services.config.database, - ) - entry = write_backup_file(server.paths, filename, data) + if BACKUP_LOCK.locked(): + raise OctopError( + ErrorCode.BACKUP_IN_PROGRESS, + "a backup is already in progress", + ) + async with BACKUP_LOCK: + data, filename = create_system_backup( + paths=server.paths, + agent_rows=_agent_rows(server), + pool=server.services.db, + db_config=server.services.config.database, + ) + entry = write_backup_file(server.paths, filename, data) return {"ok": True, "item": entry.to_dict()} @@ -95,7 +172,7 @@ async def create_backup( ) async def download_backup_file( filename: str, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> StreamingResponse: """Stream a backup file from ``backups_dir``.""" @@ -112,7 +189,7 @@ async def download_backup_file( async def restore_backup_file( filename: str, restore_config: bool = Query(default=True), - user: Any = Depends(current_admin), + user: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Restore database and workspaces from a file in ``backups_dir``. @@ -148,7 +225,7 @@ async def restore_backup_file( @router.delete("/backup/files/{filename}", summary="Delete a stored backup", status_code=204) async def remove_backup_file( filename: str, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> None: """Remove a backup archive from ``backups_dir``.""" @@ -162,7 +239,7 @@ async def remove_backup_file( response_class=StreamingResponse, ) async def export_backup( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> StreamingResponse: """Create and stream a backup without persisting to ``backups_dir``.""" @@ -183,7 +260,7 @@ async def export_backup( @router.post("/backup/import", summary="Upload backup archive to backups dir") async def import_backup( file: UploadFile = File(...), # noqa: B008 - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Save an uploaded ``.tar.gz`` into ``backups_dir`` (does not restore).""" diff --git a/src/octop/api/routers/browser/sessions.py b/src/octop/api/routers/browser/sessions.py index e6867e41..2d411fb3 100644 --- a/src/octop/api/routers/browser/sessions.py +++ b/src/octop/api/routers/browser/sessions.py @@ -34,7 +34,7 @@ from fastapi.responses import Response, StreamingResponse from pydantic import BaseModel -from octop.api.deps import current_user, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.errors import ErrorCode, OctopError from octop.infra.server import OctopServer @@ -319,7 +319,7 @@ def _verify_browser_binary(exe: str) -> tuple[bool, str]: @router.post("/browser/install") -async def install(_: Any = Depends(current_user)) -> StreamingResponse: +async def install(_: Any = Depends(require_permission("browser"))) -> StreamingResponse: """Stream Chromium install progress as SSE. Each event is a JSON line conforming to ``harness_browser.InstallEvent``: diff --git a/src/octop/api/routers/browser/uninstall.py b/src/octop/api/routers/browser/uninstall.py index e3f5e2d8..9fdf094d 100644 --- a/src/octop/api/routers/browser/uninstall.py +++ b/src/octop/api/routers/browser/uninstall.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.browser.setup import uninstall_browser_stream from octop.infra.users.identity import User from octop.infra.utils.locale import resolve_request_locale @@ -18,7 +18,7 @@ @router.post("/browser/uninstall") async def uninstall_browser( request: Request, - _user: User = Depends(current_admin), + _user: User = Depends(require_permission("browser")), ) -> StreamingResponse: """Stream browser uninstall progress as SSE (admin only).""" locale = resolve_request_locale(request) diff --git a/src/octop/api/routers/channels.py b/src/octop/api/routers/channels.py index 9b1ded47..492932e2 100644 --- a/src/octop/api/routers/channels.py +++ b/src/octop/api/routers/channels.py @@ -16,8 +16,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Request from pydantic import BaseModel, ValidationError -from octop.api.common.agent import require_agent_row -from octop.api.deps import current_user, get_server +from octop.api.common.agent import require_agent_owner_row +from octop.api.deps import get_server, require_permission from octop.infra.errors import ErrorCode, OctopError from octop.infra.gateway.channels import qr_bind from octop.infra.gateway.gateway import ChannelKind @@ -100,7 +100,7 @@ def _require_agent_access( as_user: int | None, server: Any, ) -> Any: - return require_agent_row(agent_id, user=user, as_user=as_user, server=server) + return require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) def _acting_user_id(user: Any, as_user: int | None) -> int: @@ -112,7 +112,7 @@ async def list_channels( agent_id: str, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: _require_agent_access(agent_id, user=user, as_user=as_user, server=server) @@ -128,7 +128,7 @@ async def create_channel( body: ChannelCreateBody, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: _require_agent_access(agent_id, user=user, as_user=as_user, server=server) @@ -155,7 +155,7 @@ async def get_channel( channel_id: str, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: _require_agent_access(agent_id, user=user, as_user=as_user, server=server) @@ -174,7 +174,7 @@ async def patch_channel( body: ChannelPatchBody, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: _require_agent_access(agent_id, user=user, as_user=as_user, server=server) @@ -198,7 +198,7 @@ async def delete_channel( agent_id: str, channel_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> None: _require_agent_access(agent_id, user=user, as_user=as_user, server=server) @@ -214,7 +214,7 @@ async def test_channel( channel_id: str, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Probe a channel via Gateway.probe_channel().""" @@ -236,7 +236,7 @@ async def probe_channel_config( body: ChannelProbeBody, request: Request, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Probe channel credentials from a draft config (no save required).""" @@ -413,7 +413,7 @@ def _get_plat_code() -> int: async def wecom_qrcode_generate( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Generate WeCom AI Bot QR code for registration.""" @@ -432,7 +432,7 @@ async def wecom_qrcode_poll( agent_id: str, as_user: int | None = None, scode: str = Body(..., embed=True), - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Poll WeCom QR scan result.""" @@ -457,7 +457,7 @@ async def wecom_qrcode_poll( async def qq_qrcode_generate( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Create an in-memory QQ Bot binding session and return its QR target URL.""" @@ -479,7 +479,7 @@ async def qq_qrcode_poll( agent_id: str, as_user: int | None = None, qrcode_token: str = Body(..., embed=True), - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Poll the active QQ Bot binding task and return credentials after confirmation.""" @@ -514,7 +514,7 @@ def _get_weixin_qr_login() -> Any: async def weixin_qrcode_generate( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Generate WeChat iLink Bot QR code.""" @@ -541,7 +541,7 @@ async def weixin_qrcode_poll( agent_id: str, as_user: int | None = None, qrcode_token: str = Body(..., embed=True), - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Poll WeChat QR scan result (single long-poll, ~40s).""" @@ -596,7 +596,7 @@ async def feishu_bot_creator_start( agent_id: str, as_user: int | None = None, body: dict[str, Any] = Body(default=None), - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Start the Feishu bot creator subprocess.""" @@ -664,7 +664,7 @@ async def feishu_bot_creator_start( async def feishu_bot_creator_poll( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Poll feishu bot creator subprocess for new output.""" @@ -732,7 +732,7 @@ async def feishu_bot_creator_poll( async def feishu_bot_creator_stop( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Stop the feishu bot creator subprocess.""" @@ -771,7 +771,7 @@ async def yuanbao_bot_creator_start( agent_id: str, as_user: int | None = None, body: dict[str, Any] = Body(default=None), - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Start the YuanBao bot creator subprocess.""" @@ -827,7 +827,7 @@ async def yuanbao_bot_creator_start( async def yuanbao_bot_creator_poll( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Poll YuanBao bot creator subprocess for new output.""" @@ -892,7 +892,7 @@ async def yuanbao_bot_creator_poll( async def yuanbao_bot_creator_stop( agent_id: str, as_user: int | None = None, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("channels")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Stop the YuanBao bot creator subprocess.""" diff --git a/src/octop/api/routers/chat/history.py b/src/octop/api/routers/chat/history.py index 0406112f..6967bf7a 100644 --- a/src/octop/api/routers/chat/history.py +++ b/src/octop/api/routers/chat/history.py @@ -4,20 +4,22 @@ from typing import Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from octop.api.common.agent import require_agent_row from octop.api.deps import current_user, get_server -from octop.api.routers.chat.models import RebindSessionBody, RenameThreadBody +from octop.api.routers.chat.models import ForkThreadBody, RebindSessionBody, RenameThreadBody from octop.api.routers.chat.serialize import ( HISTORY_DEFAULT_LIMIT, _clamp_history_limit, _load_thread_messages, ) from octop.infra.agents.context_breakdown import SEGMENT_KEYS, compute_context_breakdown +from octop.infra.agents.thread_fork import fork_dashboard_thread from octop.infra.errors import ErrorCode, OctopError from octop.infra.gateway.hitl.coordinator import pending_hitl_payload from octop.infra.gateway.threads import ThreadRegistry, thread_row_has_messages +from octop.infra.utils.locale import resolve_request_locale router = APIRouter() @@ -29,6 +31,9 @@ def _require_thread( row = server.app_runtime.gateway.thread_registry.get_thread(thread_id) if row is None or row.agent_id != agent_id: raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"thread {thread_id!r} not found") + effective_uid = as_user if as_user is not None else user.id + if row.user_id != effective_uid: + raise OctopError(ErrorCode.FORBIDDEN, "thread not owned by user") return row @@ -43,8 +48,8 @@ async def list_threads( """List conversation threads for an agent, including which thread is active for this user.""" require_agent_row(agent_id, user=user, as_user=as_user, server=server) thread_registry = server.app_runtime.gateway.thread_registry - rows = thread_registry.list_threads(agent_id=agent_id, limit=limit) effective_uid = as_user if as_user is not None else user.id + rows = thread_registry.list_threads(agent_id=agent_id, user_id=effective_uid, limit=limit) bound = thread_registry.get_bound_thread_id( ThreadRegistry.dashboard_key(agent_id=agent_id, user_id=effective_uid) ) @@ -82,7 +87,7 @@ async def create_thread( agent_id=agent_id, user_id=effective_uid, channel_type=ThreadRegistry.CHANNEL_DASHBOARD, - channel_subject_id=str(user.id), + channel_subject_id=str(effective_uid), ) return {"thread_id": tid, "session_key": sk} @@ -196,6 +201,41 @@ async def mark_thread_read( server.app_runtime.gateway.thread_registry.mark_thread_read(thread_id) +@router.post( + "/agents/{agent_id}/threads/{thread_id}/fork", + status_code=201, + summary="Fork thread from a user message", +) +async def fork_thread( + agent_id: str, + thread_id: str, + body: ForkThreadBody, + request: Request, + as_user: int | None = None, + user: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Create a new dashboard thread with history strictly before *message_id*. + + The original thread is left unchanged. The selected user question is not + copied — the client should prefill the composer so the user can edit and + send a different follow-up. + """ + row = _require_thread(server, agent_id, thread_id, user, as_user) + effective_uid = as_user if as_user is not None else user.id + harness = server.app_runtime.agent_registry.get_agent(agent_id) + return await fork_dashboard_thread( + thread_registry=server.app_runtime.gateway.thread_registry, + harness=harness, + source=row, + user_id=effective_uid, + message_id=body.message_id, + content=body.content, + user_turns_from_end=body.user_turns_from_end, + locale=resolve_request_locale(request), + ) + + @router.patch("/agents/{agent_id}/session", summary="Rebind dashboard session") async def rebind_session( agent_id: str, @@ -204,11 +244,8 @@ async def rebind_session( server: Any = Depends(get_server), ) -> dict[str, Any]: """Point the user's dashboard session at an existing thread.""" - require_agent_row(agent_id, user=user, as_user=None, server=server) + _require_thread(server, agent_id, body.thread_id, user, as_user=None) sk = ThreadRegistry.dashboard_key(agent_id=agent_id, user_id=user.id) - row = server.app_runtime.gateway.thread_registry.get_thread(body.thread_id) - if row is None or row.agent_id != agent_id: - raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"thread {body.thread_id!r} not found") await server.app_runtime.gateway.thread_registry.rebind( session_key=sk, thread_id=body.thread_id, agent_id=agent_id ) diff --git a/src/octop/api/routers/chat/models.py b/src/octop/api/routers/chat/models.py index b0c4e999..13661020 100644 --- a/src/octop/api/routers/chat/models.py +++ b/src/octop/api/routers/chat/models.py @@ -26,6 +26,10 @@ class ChatTurnBody(BaseModel): default=None, description="Connector MCP server names to attach for this request only.", ) + knowledge_base_ids: list[str] | None = Field( + default=None, + description="Knowledge base ids to retrieve for this request (empty disables defaults).", + ) skills: list[str] | None = Field( default=None, description="Skill names to enable for this request (empty list disables all skills).", @@ -61,6 +65,9 @@ def from_ws_payload(cls, payload: dict[str, Any]) -> ChatTurnBody: mcp_servers=payload.get("mcp_servers") if isinstance(payload.get("mcp_servers"), list) else None, + knowledge_base_ids=payload.get("knowledge_base_ids") + if isinstance(payload.get("knowledge_base_ids"), list) + else None, skills=payload.get("skills") if isinstance(payload.get("skills"), list) else payload.get("skills"), @@ -95,6 +102,7 @@ class UserTurnWsFrame(BaseModel): reasoning_mode: Literal["auto", "enabled", "disabled"] | None = None reasoning_effort: str | None = None mcp_servers: list[str] | None = None + knowledge_base_ids: list[str] | None = None skills: list[str] | None = None messages: list[dict[str, Any]] | None = None target_agent_ids: list[str] | None = None @@ -112,6 +120,19 @@ class RebindSessionBody(BaseModel): thread_id: str +class ForkThreadBody(BaseModel): + message_id: str = Field(..., description="Selected user message id from the chat UI.") + content: str | None = Field( + default=None, + description="Plain text of that user message, used to locate it when ids differ.", + ) + user_turns_from_end: int | None = Field( + default=None, + ge=1, + description="1 = latest user turn, 2 = second-to-last, … (preferred locator).", + ) + + class RenameThreadBody(BaseModel): title: str | None = None pinned: bool | None = None diff --git a/src/octop/api/routers/chat/turn.py b/src/octop/api/routers/chat/turn.py index 89afa2c5..f8b0d14c 100644 --- a/src/octop/api/routers/chat/turn.py +++ b/src/octop/api/routers/chat/turn.py @@ -70,6 +70,8 @@ async def resolve_thread_id( row = thread_registry.get_thread(thread_id) if row is None or row.agent_id != agent_id: raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"thread {thread_id!r} not found") + if row.user_id != user_id: + raise OctopError(ErrorCode.FORBIDDEN, "thread not owned by user") await thread_registry.rebind(session_key=sk, thread_id=thread_id, agent_id=agent_id) return thread_id, sk bound = thread_registry.get_bound_thread_id(sk) @@ -319,6 +321,9 @@ def build_dashboard_inbound( if prepared.mcp_servers is not None: # Including [] — Dashboard opt-out of default_open for this turn. metadata["mcp_servers"] = list(prepared.mcp_servers) + if turn.knowledge_base_ids is not None: + # Including [] — Dashboard opt-out of default_open knowledge bases for this turn. + metadata["knowledge_base_ids"] = list(turn.knowledge_base_ids) if prepared.skills is not None: metadata["skills"] = prepared.skills if prepared.model_ref: diff --git a/src/octop/api/routers/chat/ws.py b/src/octop/api/routers/chat/ws.py index edd9198e..02fcb790 100644 --- a/src/octop/api/routers/chat/ws.py +++ b/src/octop/api/routers/chat/ws.py @@ -108,7 +108,7 @@ async def send_frame(frame: dict[str, Any]) -> None: await send_frame({"type": "error", "message": "subscribe requires thread_id"}) continue row = gateway.thread_registry.get_thread(thread_id) - if row is None or row.agent_id != agent_id: + if row is None or row.agent_id != agent_id or row.user_id != user.id: await send_frame( {"type": "error", "message": f"thread {thread_id!r} not found"}, ) @@ -129,7 +129,7 @@ async def send_frame(frame: dict[str, Any]) -> None: await send_frame({"type": "error", "message": "cancel requires thread_id"}) continue row = gateway.thread_registry.get_thread(thread_id) - if row is None or row.agent_id != agent_id: + if row is None or row.agent_id != agent_id or row.user_id != user.id: await send_frame( {"type": "error", "message": f"thread {thread_id!r} not found"}, ) diff --git a/src/octop/api/routers/connectors.py b/src/octop/api/routers/connectors.py index eaa07c9e..0db565ac 100644 --- a/src/octop/api/routers/connectors.py +++ b/src/octop/api/routers/connectors.py @@ -13,7 +13,7 @@ from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field -from octop.api.deps import current_admin, current_user, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.connectors.builder import ( mcp_server_name, normalize_weiyun_mcp_token, @@ -72,9 +72,9 @@ class CreateInstanceBody(BaseModel): default_open: bool = Field( default=False, description=( - "When true, inject tools by default on IM and on Cron jobs with no " - "explicit connector picks. Dashboard and Cron with explicit picks " - "follow the user selection." + "Per-account default: when true, inject this connector's tools for " + "the owner on IM and on Cron jobs with no explicit connector picks. " + "Dashboard and Cron with explicit picks follow the selection." ), ) @@ -84,8 +84,9 @@ class PatchInstanceBody(BaseModel): default_open: bool | None = Field( default=None, description=( - "When set, update default-open. IM / empty Cron follow it; " - "Dashboard and Cron with explicit picks use the selection." + "When set, update per-account default-open. IM / empty Cron follow " + "it for this user; Dashboard and Cron with explicit picks use the " + "selection." ), ) @@ -744,7 +745,7 @@ async def connector_cli_status( ) async def connector_install_cli( kind: str, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("connectors")), ) -> dict[str, Any]: """Run ``npm install -g`` for the connector CLI on the Octop host (admin only). diff --git a/src/octop/api/routers/cron.py b/src/octop/api/routers/cron.py index e6eca885..3f78e710 100644 --- a/src/octop/api/routers/cron.py +++ b/src/octop/api/routers/cron.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel, Field -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.deps import current_user, get_server from octop.infra.cron.task_type import normalize_cron_task_type, require_cron_prompt from octop.infra.cron.trigger import build_trigger @@ -59,7 +59,7 @@ async def list_cron( server: Any = Depends(get_server), ) -> list[dict[str, Any]]: """List scheduled jobs for an agent.""" - require_agent_row(agent_id, user=user, as_user=None, server=server) + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) return [ r.to_public_dict(include_agent=True) for r in _get_cron_manager(server).list_by_agent(agent_id) @@ -77,6 +77,7 @@ async def create_cron( from octop.api.common.validators import validate_chat_mcp_servers # noqa: PLC0415 from octop.infra.cron.manager import CronCreateSpec # noqa: PLC0415 + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) mcp_servers = ( await validate_chat_mcp_servers(server, user_id=user.id, names=body.mcp_servers) or [] ) @@ -105,6 +106,7 @@ async def get_cron( server: Any = Depends(get_server), ) -> dict[str, Any]: """Return one scheduled job by id.""" + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) row = _get_cron_manager(server).get(cron_id) if row is None or row.agent_id != agent_id: raise OctopError(ErrorCode.NOT_FOUND, "cron job not found") @@ -123,6 +125,7 @@ async def patch_cron( from octop.api.common.validators import validate_chat_mcp_servers # noqa: PLC0415 from octop.infra.db.repos._base import UNSET # noqa: PLC0415 + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) mgr = _get_cron_manager(server) existing = mgr.get(cron_id) if existing is None or existing.agent_id != agent_id: @@ -157,6 +160,7 @@ async def delete_cron( server: Any = Depends(get_server), ) -> None: """Remove a scheduled job.""" + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) mgr = _get_cron_manager(server) existing = mgr.get(cron_id) if existing is None or existing.agent_id != agent_id: @@ -172,6 +176,7 @@ async def run_now( server: Any = Depends(get_server), ) -> None: """Trigger an immediate one-off run without waiting for the schedule.""" + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) mgr = _get_cron_manager(server) existing = mgr.get(cron_id) if existing is None or existing.agent_id != agent_id: diff --git a/src/octop/api/routers/desktop/install.py b/src/octop/api/routers/desktop/install.py index fda38f19..e4ba1db8 100644 --- a/src/octop/api/routers/desktop/install.py +++ b/src/octop/api/routers/desktop/install.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.desktop.setup import install_desktop_stream from octop.infra.users.identity import User from octop.infra.utils.locale import resolve_request_locale @@ -18,7 +18,7 @@ @router.post("/desktop/install") async def install_desktop( request: Request, - _user: User = Depends(current_admin), + _user: User = Depends(require_permission("desktop")), ) -> StreamingResponse: """Stream virtual desktop installation progress as SSE (admin only). diff --git a/src/octop/api/routers/desktop/settings.py b/src/octop/api/routers/desktop/settings.py index 67853d01..8ccbca68 100644 --- a/src/octop/api/routers/desktop/settings.py +++ b/src/octop/api/routers/desktop/settings.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field, field_validator -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.desktop.setup import apply_geometry, desktop_status, parse_geometry, read_geometry from octop.infra.users.identity import User @@ -28,7 +28,7 @@ def validate_geometry(cls, value: str) -> str: @router.post("/desktop/geometry") async def set_desktop_geometry( body: DesktopGeometryRequest, - _user: User = Depends(current_admin), + _user: User = Depends(require_permission("desktop")), ) -> dict[str, object]: """Resize the Linux virtual desktop (restarts Xvnc). Admin only.""" if desktop_status().platform != "linux": @@ -41,7 +41,9 @@ async def set_desktop_geometry( @router.get("/desktop/geometry") -async def get_desktop_geometry(_user: User = Depends(current_admin)) -> dict[str, object]: +async def get_desktop_geometry( + _user: User = Depends(require_permission("desktop")), +) -> dict[str, object]: geometry = read_geometry() w, h = parse_geometry(geometry) return {"geometry": geometry, "width": w, "height": h} diff --git a/src/octop/api/routers/desktop/status.py b/src/octop/api/routers/desktop/status.py index 999385da..f0bf6167 100644 --- a/src/octop/api/routers/desktop/status.py +++ b/src/octop/api/routers/desktop/status.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, Request -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.desktop.session import active_session_count, session_limit from octop.infra.desktop.setup import desktop_status from octop.infra.users.identity import User @@ -16,7 +16,7 @@ @router.get("/desktop/status") async def get_desktop_status( request: Request, - _user: User = Depends(current_admin), + _user: User = Depends(require_permission("desktop")), ) -> dict[str, object]: locale = resolve_request_locale(request) status = desktop_status(locale=locale) diff --git a/src/octop/api/routers/desktop/stream.py b/src/octop/api/routers/desktop/stream.py index 9b252312..7f75eb6f 100644 --- a/src/octop/api/routers/desktop/stream.py +++ b/src/octop/api/routers/desktop/stream.py @@ -28,6 +28,7 @@ from octop.infra.desktop.setup import desktop_status from octop.infra.errors import ErrorCode, OctopError from octop.infra.users.identity import User +from octop.infra.users.permissions import user_has_permission from octop.infra.utils.locale import resolve_request_locale logger = logging.getLogger(__name__) @@ -284,8 +285,8 @@ async def _force_disconnect() -> None: ) await websocket.close(code=4001, reason=f"auth failed: {exc}") return - if not user.is_admin: - await websocket.close(code=4003, reason="admin required") + if not user_has_permission(user, "desktop"): + await websocket.close(code=4003, reason="permission required") return user_id = str(user.id) diff --git a/src/octop/api/routers/desktop/uninstall.py b/src/octop/api/routers/desktop/uninstall.py index bdf758ae..94c40ea8 100644 --- a/src/octop/api/routers/desktop/uninstall.py +++ b/src/octop/api/routers/desktop/uninstall.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.desktop.setup import uninstall_desktop_stream from octop.infra.users.identity import User from octop.infra.utils.locale import resolve_request_locale @@ -18,7 +18,7 @@ @router.post("/desktop/uninstall") async def uninstall_desktop( request: Request, - _user: User = Depends(current_admin), + _user: User = Depends(require_permission("desktop")), ) -> StreamingResponse: """Stream virtual desktop uninstall progress as SSE (admin only).""" locale = resolve_request_locale(request) diff --git a/src/octop/api/routers/envs.py b/src/octop/api/routers/envs.py index 7c5e097f..a4fccf55 100644 --- a/src/octop/api/routers/envs.py +++ b/src/octop/api/routers/envs.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Body, Depends -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission from octop.infra.errors import ErrorCode, OctopError from octop.infra.utils.env_file import ( apply_env_file, @@ -25,7 +25,7 @@ @router.get("") async def list_envs( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("envs")), server: Any = Depends(get_server), ) -> list[dict[str, str]]: path = env_file_path(server.paths.root) @@ -35,7 +35,7 @@ async def list_envs( @router.put("") async def batch_save_envs( body: dict[str, str] = Body(...), - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("envs")), server: Any = Depends(get_server), ) -> list[dict[str, str]]: cleaned: dict[str, str] = {} @@ -55,7 +55,7 @@ async def batch_save_envs( @router.delete("/{key}") async def delete_env( key: str, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("envs")), server: Any = Depends(get_server), ) -> list[dict[str, str]]: k = key.strip() diff --git a/src/octop/api/routers/experts.py b/src/octop/api/routers/experts.py index a8ab2566..989390da 100644 --- a/src/octop/api/routers/experts.py +++ b/src/octop/api/routers/experts.py @@ -14,14 +14,19 @@ import asyncio from typing import Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Response from pydantic import BaseModel, Field +from octop.api.common.agent import require_agent_owner_row, user_owns_agent from octop.api.common.agent_runtime import AgentRuntimeFields, runtime_field_updates from octop.api.deps import current_user, get_server from octop.infra.agents.experts.catalog import ( + MANIFEST_FILENAME, build_create_spec_from_expert, + discover_seed_paths, preview_file_paths, + preview_paths_from_expert_dir, + read_text_file_contents, ) from octop.infra.agents.experts.market_creation import ( SkillHubMarketAgentCreateOptions, @@ -29,6 +34,22 @@ from octop.infra.agents.experts.market_creation import ( create_agent_from_skillhub_skillset as create_skillhub_market_agent, ) +from octop.infra.agents.experts.published_creation import ( + PublishedExpertInstallOptions, + require_published_expert, +) +from octop.infra.agents.experts.published_creation import ( + install_published_expert as install_published_expert_agent, +) +from octop.infra.agents.experts.published_creation import ( + publish_agent_expert as publish_owned_agent_expert, +) +from octop.infra.agents.experts.published_creation import ( + refresh_published_expert as refresh_owned_published_expert, +) +from octop.infra.agents.experts.published_creation import ( + unpublish_expert as unpublish_owned_expert, +) from octop.infra.agents.experts.skillhub_market import ( SkillHubMarketError, SkillHubMarketErrorKind, @@ -59,6 +80,34 @@ class FromExpertBody(AgentRuntimeFields): default_model: str | None = None backend: dict[str, Any] | None = None skill_package_ids: list[str] | None = None + color: str | None = None + + +class PublishExpertBody(BaseModel): + name: str = Field(min_length=1) + description: str = "" + slug: str | None = None + welcome_message: LocalizedTextResponse | None = None + quick_prompts: list[QuickPromptResponse] | None = None + + +class RefreshPublishedExpertBody(BaseModel): + name: str | None = Field(default=None, min_length=1) + description: str | None = None + welcome_message: LocalizedTextResponse | None = None + quick_prompts: list[QuickPromptResponse] | None = None + + +class InstallPublishedExpertBody(AgentRuntimeFields): + """Same create knobs as bundled / market experts, plus required name.""" + + name: str = Field(min_length=1) + description: str = "" + providers: list[str] | None = None + default_model: str | None = None + backend: dict[str, Any] | None = None + skill_package_ids: list[str] | None = None + color: str | None = None class LocalizedTextResponse(BaseModel): @@ -128,6 +177,24 @@ def _quick_prompt_dict(p: Any) -> dict[str, Any]: } +def _quick_prompt_body_dict(p: QuickPromptResponse) -> dict[str, Any]: + return { + "title": {"zh": p.title.zh, "en": p.title.en}, + "description": {"zh": p.description.zh, "en": p.description.en}, + "prompt": {"zh": p.prompt.zh, "en": p.prompt.en}, + "color": p.color, + "icon_name": p.icon_name, + } + + +def _publish_welcome_fields( + welcome: LocalizedTextResponse | None, +) -> tuple[str, str]: + if welcome is None: + return "", "" + return welcome.zh, welcome.en + + def _summary_dict(s: Any) -> dict[str, Any]: return { "id": s.id, @@ -186,6 +253,205 @@ def _map_skillhub_error(exc: SkillHubMarketError) -> OctopError: ) +def _published_snapshot_dir(server: Any, expert_id: str) -> Any: + return server.services.paths.published_experts_dir / expert_id + + +def _published_creator_username(server: Any, created_by: str) -> str | None: + try: + user_id = int(created_by) + except ValueError: + return None + user = server.services.user_repo.get(user_id) + return user.username if user is not None else None + + +def _published_summary_dict(row: Any, server: Any) -> dict[str, Any]: + return { + "id": row.id, + "slug": row.slug, + "name": row.name, + "description": row.description, + "created_by": row.created_by, + "creator_username": _published_creator_username(server, row.created_by), + "source_agent_id": row.source_agent_id, + "icon_name": row.icon_name or None, + "color": row.color or None, + "created_at": row.created_at, + "updated_at": row.updated_at, + } + + +def _require_published_expert(server: Any, expert_id: str) -> Any: + assert server.services is not None + return require_published_expert(server.services, expert_id) + + +@router.get("/experts/published", summary="List published expert templates") +async def list_published_experts( + _: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> list[dict[str, Any]]: + """List expert snapshots published by users and available for private installation.""" + return [ + _published_summary_dict(row, server) + for row in server.services.published_expert_repo.list_all() + ] + + +@router.get("/experts/published/{expert_id}", summary="Get published expert template detail") +async def get_published_expert( + expert_id: str, + _: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Return published-expert metadata and a previewable snapshot file inventory.""" + row = _require_published_expert(server, expert_id) + snapshot_dir = _published_snapshot_dir(server, row.id) + preview_paths = await asyncio.to_thread(preview_paths_from_expert_dir, snapshot_dir) + files = await asyncio.to_thread(discover_seed_paths, snapshot_dir) + if (snapshot_dir / MANIFEST_FILENAME).is_file(): + files.insert(0, MANIFEST_FILENAME) + return { + **_published_summary_dict(row, server), + "files": files, + "file_contents": await asyncio.to_thread( + read_text_file_contents, + snapshot_dir, + preview_paths, + ), + } + + +@router.post( + "/agents/{agent_id}/publish-expert", + status_code=201, + summary="Publish an agent workspace as an expert template", +) +async def publish_agent_expert( + agent_id: str, + body: PublishExpertBody, + user: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Snapshot an owned agent workspace into a globally installable expert template.""" + source = require_agent_owner_row(agent_id, user=user, as_user=None, server=server) + if not user_owns_agent(source, user): + raise OctopError(ErrorCode.FORBIDDEN, "agent not owned by user") + assert server.app_runtime is not None + assert server.services is not None + workspace = server.app_runtime.agent_registry.workspace_for_agent(agent_id) + if workspace is None: + raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {agent_id!r} not found") + welcome_zh, welcome_en = _publish_welcome_fields(body.welcome_message) + row = await publish_owned_agent_expert( + services=server.services, + registry=server.app_runtime.agent_registry, + user=user, + source=source, + workspace=workspace, + name=body.name, + description=body.description, + slug=body.slug, + welcome_message_zh=welcome_zh, + welcome_message_en=welcome_en, + quick_prompts=tuple(_quick_prompt_body_dict(p) for p in (body.quick_prompts or [])), + ) + return _published_summary_dict(row, server) + + +@router.post( + "/experts/published/{expert_id}/refresh", + summary="Refresh a published expert snapshot", +) +async def refresh_published_expert( + expert_id: str, + body: RefreshPublishedExpertBody | None = None, + user: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Replace a published snapshot using its still-owned source agent workspace.""" + row = _require_published_expert(server, expert_id) + if row.source_agent_id is None: + raise OctopError(ErrorCode.NOT_FOUND, "published expert source agent not found") + source = require_agent_owner_row(row.source_agent_id, user=user, as_user=None, server=server) + assert server.app_runtime is not None + assert server.services is not None + workspace = server.app_runtime.agent_registry.workspace_for_agent(row.source_agent_id) + if workspace is None: + raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {row.source_agent_id!r} not found") + welcome_zh: str | None = None + welcome_en: str | None = None + quick_prompts: tuple[dict[str, Any], ...] | None = None + if body is not None: + if body.welcome_message is not None: + welcome_zh, welcome_en = _publish_welcome_fields(body.welcome_message) + if body.quick_prompts is not None: + quick_prompts = tuple(_quick_prompt_body_dict(p) for p in body.quick_prompts) + updated = await refresh_owned_published_expert( + services=server.services, + registry=server.app_runtime.agent_registry, + user=user, + expert_id=expert_id, + source=source, + workspace=workspace, + name=body.name if body is not None else None, + description=body.description if body is not None else None, + welcome_message_zh=welcome_zh, + welcome_message_en=welcome_en, + quick_prompts=quick_prompts, + ) + return _published_summary_dict(updated, server) + + +@router.delete( + "/experts/published/{expert_id}", + status_code=204, + summary="Unpublish an expert template", +) +async def unpublish_expert( + expert_id: str, + user: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> Response: + """Remove a published expert's listing and snapshot without deleting installed forks.""" + assert server.services is not None + await unpublish_owned_expert(services=server.services, user=user, expert_id=expert_id) + return Response(status_code=204) + + +@router.post( + "/experts/published/{expert_id}/install", + status_code=201, + summary="Install a private agent from a published expert", +) +async def install_published_expert( + expert_id: str, + body: InstallPublishedExpertBody, + user: Any = Depends(current_user), + server: Any = Depends(get_server), +) -> dict[str, Any]: + """Create a private agent and seed it from the immutable published snapshot.""" + assert server.app_runtime is not None + assert server.services is not None + return await install_published_expert_agent( + services=server.services, + registry=server.app_runtime.agent_registry, + user=user, + expert_id=expert_id, + options=PublishedExpertInstallOptions( + name=body.name, + description=body.description, + providers=body.providers, + default_model=body.default_model, + backend=body.backend, + skill_package_ids=body.skill_package_ids, + color=body.color, + runtime_config=runtime_field_updates(body, exclude_unset=True), + ), + ) + + @router.get("/experts") async def list_experts( _: Any = Depends(current_user), @@ -267,6 +533,7 @@ async def install_expert_hub_item( providers=body.providers, default_model=body.default_model, backend=body.backend, + color=body.color, **runtime_field_updates(body, exclude_unset=False), ), ) @@ -336,6 +603,8 @@ async def create_agent_from_expert( config_extra["providers"] = list(body.providers) if body.backend: config_extra["backend"] = body.backend + if body.color: + config_extra["color"] = body.color locale = resolve_user_locale( user_repo=server.services.user_repo, diff --git a/src/octop/api/routers/knowledge_bases.py b/src/octop/api/routers/knowledge_bases.py new file mode 100644 index 00000000..78223408 --- /dev/null +++ b/src/octop/api/routers/knowledge_bases.py @@ -0,0 +1,553 @@ +"""HTTP API for private, shareable knowledge bases.""" + +from __future__ import annotations + +from contextlib import suppress +from dataclasses import asdict +from typing import Any + +from fastapi import APIRouter, Depends, File, Query, Request, UploadFile, status +from pydantic import BaseModel, Field + +from octop.api.deps import current_user, get_server, require_permission +from octop.infra.agents.providers.model_flags import is_embedding_model, is_onnx_local_provider +from octop.infra.agents.providers.onnx_catalog import ( + ONNX_PRESET_MODEL_IDS, + list_onnx_catalog_models, +) +from octop.infra.agents.providers.onnx_service import ( + DOWNLOAD_MANAGER, + OnnxServiceConfig, + assert_catalog_model, + ensure_local_embedding_deps_async, + is_model_downloaded, + save_config, + status_payload, +) +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.knowledge.gate import ( + assert_knowledge_usable, + get_capability, + set_feature_enabled, +) +from octop.infra.knowledge.jobs import enqueue_index_document, reindex_all_documents +from octop.infra.knowledge.service import ( + MAX_BASES_PER_OWNER, + MAX_DOCS_PER_KB, + MAX_DOCUMENT_BYTES, + KnowledgeService, +) +from octop.infra.server import OctopServer +from octop.infra.users.identity import User +from octop.infra.utils.locale import resolve_request_locale + +router = APIRouter(prefix="/knowledge-bases") + + +class FeatureBody(BaseModel): + enabled: bool = Field(description="Whether to enable the instance-wide knowledge-base feature.") + model: str | None = Field( + default=None, + description="Downloaded ONNX embedding model ID; required when enabling.", + ) + backend: str | None = Field(default=None, pattern="^(onnx|remote)$") + provider_id: str | None = None + + +class OnnxDownloadBody(BaseModel): + model: str = Field(min_length=1, description="Catalog ONNX embedding model id to download.") + + +class CreateBaseBody(BaseModel): + name: str = Field(min_length=1, max_length=200) + description: str = Field(default="", max_length=2000) + default_open: bool = False + shared: bool = False + icon_name: str = Field(default="", max_length=64) + + +class UpdateBaseBody(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=200) + description: str | None = Field(default=None, max_length=2000) + default_open: bool | None = None + shared: bool | None = None + icon_name: str | None = Field(default=None, max_length=64) + + +def _knowledge_service(server: OctopServer) -> KnowledgeService: + if server.services is None: + raise OctopError(ErrorCode.INTERNAL_ERROR, "knowledge services are not initialized") + return KnowledgeService(server.services) + + +def _row_payload(row: Any) -> dict[str, Any]: + return asdict(row) + + +def _owner_fields(server: OctopServer, owner_user_id: int) -> dict[str, str | None]: + if server.services is None: + return {"owner_username": None, "owner_display_name": None} + user_repo = getattr(server.services, "user_repo", None) + if user_repo is None: + return {"owner_username": None, "owner_display_name": None} + owner = user_repo.get(int(owner_user_id)) + if owner is None: + return {"owner_username": None, "owner_display_name": None} + return { + "owner_username": owner.username, + "owner_display_name": owner.display_name or owner.username, + } + + +def _base_payload(server: OctopServer, row: Any) -> dict[str, Any]: + return {**asdict(row), **_owner_fields(server, row.owner_user_id)} + + +def _is_admin(user: User) -> bool: + return bool(user.is_admin) + + +def _map_knowledge_error(exc: Exception, *, locale: str) -> OctopError: + if isinstance(exc, OctopError): + return exc + if isinstance(exc, LookupError): + return OctopError.localized(ErrorCode.KNOWLEDGE_NOT_FOUND, locale) + if isinstance(exc, PermissionError): + return OctopError.localized(ErrorCode.KNOWLEDGE_FORBIDDEN, locale) + text = str(exc).lower() + if isinstance(exc, RuntimeError): + code = ( + ErrorCode.KNOWLEDGE_FEATURE_DISABLED + if "disabled" in text + else ErrorCode.KNOWLEDGE_PREREQUISITES_FAILED + ) + return OctopError.localized(code, locale) + if "at most 100" in text: + return OctopError.localized(ErrorCode.KNOWLEDGE_DOC_LIMIT, locale) + if "document size exceeds" in text: + return OctopError.localized(ErrorCode.KNOWLEDGE_DOC_TOO_LARGE, locale) + if "at most" in text and "knowledge bases" in text: + return OctopError.localized(ErrorCode.KNOWLEDGE_BASE_LIMIT, locale) + if "unsupported knowledge document content type" in text: + return OctopError.localized(ErrorCode.KNOWLEDGE_UNSUPPORTED_TYPE, locale) + if "unique constraint" in text or "duplicate key" in text: + return OctopError.localized(ErrorCode.KNOWLEDGE_NAME_TAKEN, locale) + return OctopError.localized(ErrorCode.KNOWLEDGE_PREREQUISITES_FAILED, locale) + + +def _onnx_options_for_knowledge(*, include_all: bool = False) -> list[dict[str, Any]]: + """Recommended ONNX models, or the full catalog when *include_all* is set.""" + catalog = list_onnx_catalog_models() + if include_all: + models = catalog + else: + by_id = {str(model["id"]): model for model in catalog} + models = [by_id[model_id] for model_id in ONNX_PRESET_MODEL_IDS if model_id in by_id] + return [ + { + **model, + "downloaded": is_model_downloaded(str(model["id"])), + "recommended": bool(model.get("recommended")), + } + for model in models + ] + + +def _enable_onnx_service(server: OctopServer, model: str) -> None: + """Turn on the local ONNX service for a downloaded catalog model.""" + assert server.services is not None + verified = assert_catalog_model(model) + if not is_model_downloaded(verified): + raise RuntimeError("ONNX embedding model is not configured or not downloaded") + save_config( + server.services.settings_repo.set, + OnnxServiceConfig(enabled=True, model=verified), + ) + + +def _require_usable(server: OctopServer, request: Request) -> None: + assert server.services is not None + try: + assert_knowledge_usable(server.services.settings_repo.get, server.services.provider_repo) + except (RuntimeError, ValueError) as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +def _capability_payload(server: OctopServer) -> dict[str, Any]: + assert server.services is not None + payload = get_capability(server.services.settings_repo.get, server.services.provider_repo) + payload["limits"] = { + "max_bases_per_owner": MAX_BASES_PER_OWNER, + "max_docs_per_kb": MAX_DOCS_PER_KB, + "max_document_bytes": MAX_DOCUMENT_BYTES, + } + return payload + + +@router.get("/capability", summary="Get knowledge-base feature capability") +async def capability( + server: OctopServer = Depends(get_server), + _user: User = Depends(current_user), +) -> dict[str, Any]: + return _capability_payload(server) + + +@router.get("/embedding-options", summary="List knowledge embedding backend options") +async def embedding_options( + all_onnx: bool = Query( + False, + description="Include the full local ONNX catalog instead of the three recommended models", + ), + server: OctopServer = Depends(get_server), + _admin: User = Depends(require_permission("knowledge_settings")), +) -> dict[str, Any]: + assert server.services is not None + remote = [] + for provider in server.services.provider_repo.list_all(): + if is_onnx_local_provider( + provider.name, provider_api_key=getattr(provider, "api_key", None) + ): + continue + models = [ + {"id": str(model["id"]), "name": str(model.get("name") or model["id"])} + for model in provider.get_models() + if str(model.get("id") or "").strip() + and is_embedding_model( + model, + provider_name=provider.name, + provider_api_key=getattr(provider, "api_key", None), + ) + ] + if models: + remote.append( + {"provider_id": str(provider.id), "provider_name": provider.name, "models": models} + ) + return { + "onnx": _onnx_options_for_knowledge(include_all=all_onnx is True), + "remote": remote, + } + + +@router.put("/feature", summary="Enable or disable knowledge bases") +async def put_feature( + body: FeatureBody, + request: Request, + server: OctopServer = Depends(get_server), + _admin: User = Depends(require_permission("knowledge_settings")), +) -> dict[str, Any]: + assert server.services is not None + previous = get_capability(server.services.settings_repo.get, server.services.provider_repo) + selected_backend = (body.backend or "onnx").strip().lower() + if body.enabled and selected_backend == "onnx": + try: + await ensure_local_embedding_deps_async(allow_install=True) + except RuntimeError as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + try: + set_feature_enabled( + server.services.settings_repo.get, + server.services.settings_repo.set, + enabled=body.enabled, + backend=body.backend, + model=body.model, + provider_id=body.provider_id, + provider_repo=server.services.provider_repo, + ) + except (RuntimeError, ValueError) as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + capability = _capability_payload(server) + if body.enabled and selected_backend == "onnx": + selected_model = str(capability.get("selected_model") or "").strip() + if selected_model: + with suppress(RuntimeError, ValueError): + _enable_onnx_service(server, selected_model) + if body.enabled and ( + not previous["feature_enabled"] + or previous["selected_model"] != capability["selected_model"] + or previous["backend"] != capability["backend"] + or previous["provider_id"] != capability["provider_id"] + ): + reindex_all_documents(server.services, capability["selected_model"]) + return capability + + +@router.post("/onnx-download", summary="Download a local ONNX embedding model") +async def start_onnx_download( + body: OnnxDownloadBody, + request: Request, + _: User = Depends(require_permission("knowledge_settings")), +) -> dict[str, Any]: + try: + state = await DOWNLOAD_MANAGER.start_download(body.model.strip()) + except (RuntimeError, ValueError) as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + return state.to_dict() + + +@router.get("/onnx-download-status", summary="Poll ONNX embedding model download progress") +async def onnx_download_status( + _: User = Depends(require_permission("knowledge_settings")), +) -> dict[str, Any]: + return DOWNLOAD_MANAGER.state.to_dict() + + +@router.post("/onnx-activate", summary="Enable local ONNX service for a downloaded model") +async def activate_onnx_service( + body: OnnxDownloadBody, + request: Request, + server: OctopServer = Depends(get_server), + _: User = Depends(require_permission("knowledge_settings")), +) -> dict[str, Any]: + try: + await ensure_local_embedding_deps_async(allow_install=True) + _enable_onnx_service(server, body.model.strip()) + except (RuntimeError, ValueError) as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + assert server.services is not None + return status_payload(server.services.settings_repo.get, DOWNLOAD_MANAGER.state) + + +@router.get("", summary="List visible knowledge bases") +async def list_bases( + server: OctopServer = Depends(get_server), + user: User = Depends(current_user), +) -> list[dict[str, Any]]: + return [ + _base_payload(server, base) + for base in _knowledge_service(server).list_visible_bases( + actor_user_id=user.id, is_admin=_is_admin(user) + ) + ] + + +@router.post("", status_code=status.HTTP_201_CREATED, summary="Create a knowledge base") +async def create_base( + body: CreateBaseBody, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> dict[str, Any]: + locale = resolve_request_locale(request) + try: + _require_usable(server, request) + base = _knowledge_service(server).create_base( + owner_user_id=user.id, + name=body.name.strip(), + description=body.description.strip(), + default_open=body.default_open, + shared=body.shared, + icon_name=body.icon_name.strip(), + ) + return _base_payload(server, base) + except Exception as exc: + raise _map_knowledge_error(exc, locale=locale) from exc + + +@router.get("/default-open", summary="List current user's default-open knowledge-base IDs") +async def default_open_bases( + server: OctopServer = Depends(get_server), + user: User = Depends(current_user), +) -> dict[str, list[str]]: + bases = _knowledge_service(server).list_visible_bases( + actor_user_id=user.id, is_admin=_is_admin(user) + ) + return { + "knowledge_base_ids": [ + base.id + for base in bases + if base.default_open and int(base.owner_user_id) == int(user.id) + ] + } + + +@router.get("/{kb_id}", summary="Get a visible knowledge base") +async def get_base( + kb_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(current_user), +) -> dict[str, Any]: + try: + return _base_payload( + server, + _knowledge_service(server).get_readable_base( + kb_id, actor_user_id=user.id, is_admin=_is_admin(user) + ), + ) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.patch("/{kb_id}", summary="Update knowledge-base settings") +async def update_base( + kb_id: str, + body: UpdateBaseBody, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> dict[str, Any]: + try: + return _base_payload( + server, + _knowledge_service(server).update_base( + kb_id, + actor_user_id=user.id, + name=body.name.strip() if body.name is not None else None, + description=body.description.strip() if body.description is not None else None, + default_open=body.default_open, + shared=body.shared, + icon_name=body.icon_name.strip() if body.icon_name is not None else None, + is_admin=_is_admin(user), + ), + ) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.delete( + "/{kb_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a knowledge base" +) +async def delete_base( + kb_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> None: + try: + _knowledge_service(server).delete_base( + kb_id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.get("/{kb_id}/documents", summary="List knowledge-base documents") +async def list_documents( + kb_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(current_user), +) -> list[dict[str, Any]]: + try: + return [ + _row_payload(document) + for document in _knowledge_service(server).list_documents( + kb_id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + ] + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.post( + "/{kb_id}/documents", status_code=status.HTTP_201_CREATED, summary="Upload a knowledge document" +) +async def upload_document( + kb_id: str, + request: Request, + upload: UploadFile = File(..., description="A supported text, PDF, DOCX, or PPTX document."), + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> dict[str, Any]: + locale = resolve_request_locale(request) + try: + _require_usable(server, request) + content = await upload.read() + document = _knowledge_service(server).upload_document( + kb_id, + actor_user_id=user.id, + filename=upload.filename or "", + content_type=upload.content_type or "", + content=content, + is_admin=_is_admin(user), + ) + assert server.services is not None + enqueue_index_document(server.services, kb_id, document.id) + return _row_payload(document) + except Exception as exc: + raise _map_knowledge_error(exc, locale=locale) from exc + + +@router.get( + "/{kb_id}/documents/{doc_id}/preview", + summary="Preview extracted document text", +) +async def preview_document( + kb_id: str, + doc_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(current_user), +) -> dict[str, Any]: + try: + return _knowledge_service(server).preview_document( + kb_id, doc_id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.delete( + "/{kb_id}/documents/{doc_id}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Delete a document", +) +async def delete_document( + kb_id: str, + doc_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> None: + try: + _knowledge_service(server).delete_document( + kb_id, doc_id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.post( + "/{kb_id}/documents/{doc_id}/reindex", + summary="Rebuild the index for one knowledge document", +) +async def reindex_document( + kb_id: str, + doc_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> dict[str, Any]: + try: + _require_usable(server, request) + document = _knowledge_service(server).reindex_document( + kb_id, doc_id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + assert server.services is not None + enqueue_index_document(server.services, kb_id, doc_id) + return _row_payload(document) + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc + + +@router.post("/{kb_id}/reindex", summary="Reindex all documents in a knowledge base") +async def reindex_base( + kb_id: str, + request: Request, + server: OctopServer = Depends(get_server), + user: User = Depends(require_permission("knowledge_bases")), +) -> dict[str, int]: + try: + _require_usable(server, request) + service = _knowledge_service(server) + service.get_writable_base(kb_id, actor_user_id=user.id, is_admin=_is_admin(user)) + documents = service.list_documents(kb_id, actor_user_id=user.id, is_admin=_is_admin(user)) + assert server.services is not None + for document in documents: + service.reindex_document( + kb_id, document.id, actor_user_id=user.id, is_admin=_is_admin(user) + ) + enqueue_index_document(server.services, kb_id, document.id) + return {"enqueued": len(documents)} + except Exception as exc: + raise _map_knowledge_error(exc, locale=resolve_request_locale(request)) from exc diff --git a/src/octop/api/routers/mbti.py b/src/octop/api/routers/mbti.py index dc9307e2..75933e1b 100644 --- a/src/octop/api/routers/mbti.py +++ b/src/octop/api/routers/mbti.py @@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel, Field +from octop.api.common.agent import assert_agent_owner from octop.api.deps import current_user, get_server from octop.infra.agents.mbti_profiles import ( MBTIProfile, @@ -38,6 +39,7 @@ def _resolve_agent_row(server: Any, user: Any, agent_id: str) -> Any: row = server.app_runtime.agent_registry.get_row(agent_id) if row is None: raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {agent_id} not found") + assert_agent_owner(row, user) return row diff --git a/src/octop/api/routers/memory.py b/src/octop/api/routers/memory.py index 60106017..87929041 100644 --- a/src/octop/api/routers/memory.py +++ b/src/octop/api/routers/memory.py @@ -39,7 +39,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query from pydantic import BaseModel, Field -from octop.api.common.agent import assert_agent_owner, require_agent_row +from octop.api.common.agent import assert_agent_owner, require_agent_owner_row from octop.api.common.memory_client import call_memory_rpc from octop.api.deps import current_user, get_server from octop.infra.errors import ErrorCode, OctopError @@ -728,7 +728,7 @@ async def get_extract_config( server: Any = Depends(get_server), ) -> dict[str, Any]: """Return the agent's extraction-trigger configuration (with defaults).""" - row = require_agent_row(agent_id, user=user, as_user=as_user, server=server) + row = require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) return _read_extract_config(row) diff --git a/src/octop/api/routers/memory_portable.py b/src/octop/api/routers/memory_portable.py index 1eb9ffb1..5fb272e0 100644 --- a/src/octop/api/routers/memory_portable.py +++ b/src/octop/api/routers/memory_portable.py @@ -18,7 +18,7 @@ from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel, Field -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.common.agent_workspace import resolve_agent_workspace_dir from octop.api.common.content_disposition import content_disposition from octop.api.common.memory_client import memory_db_path, memory_namespace @@ -128,7 +128,7 @@ async def pack_agent_memory( as_user: int | None = Query(default=None), ) -> StreamingResponse: """Package the agent's memory into a .hmpkg file and serve it as a download attachment.""" - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) _refuse_postgres_portable(server, agent_id) try: @@ -209,7 +209,7 @@ async def adopt_agent_memory( as_user: int | None = Query(default=None), ) -> JSONResponse: """Upload a .hmpkg file and import it into the target host.""" - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) _refuse_postgres_portable(server, agent_id) try: @@ -260,7 +260,7 @@ async def doctor_agent_memory( as_user: int | None = Query(default=None), ) -> JSONResponse: """Run a health check against the target db.""" - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) _refuse_postgres_portable(server, agent_id) try: diff --git a/src/octop/api/routers/observability.py b/src/octop/api/routers/observability.py index e1d295be..af687fd3 100644 --- a/src/octop/api/routers/observability.py +++ b/src/octop/api/routers/observability.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel, Field -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission router = APIRouter() @@ -50,7 +50,7 @@ def _to_response(cfg: Any) -> LangfuseConfigResponse: @router.get("/langfuse", summary="Get Langfuse observability config") async def get_langfuse_config( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("observability")), server: Any = Depends(get_server), ) -> LangfuseConfigResponse: return _to_response(server.app_runtime.agent_registry.langfuse.load()) @@ -59,7 +59,7 @@ async def get_langfuse_config( @router.put("/langfuse", summary="Update Langfuse observability config") async def put_langfuse_config( body: LangfuseConfigBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("observability")), server: Any = Depends(get_server), ) -> LangfuseConfigResponse: cfg = server.app_runtime.agent_registry.save_langfuse( @@ -74,7 +74,7 @@ async def put_langfuse_config( @router.post("/langfuse/test", summary="Test Langfuse connection") async def test_langfuse_connection( body: LangfuseTestBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("observability")), server: Any = Depends(get_server), ) -> dict[str, Any]: return cast( diff --git a/src/octop/api/routers/ollama_models.py b/src/octop/api/routers/ollama_models.py index 999d6b71..b987a804 100644 --- a/src/octop/api/routers/ollama_models.py +++ b/src/octop/api/routers/ollama_models.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field -from octop.api.deps import current_user +from octop.api.deps import get_server, require_permission from octop.api.routers.ollama_download_store import ( DownloadTask, DownloadTaskStatus, @@ -76,11 +76,17 @@ def _task_to_response(task: DownloadTask) -> OllamaDownloadTaskResponse: summary="List Ollama models", ) async def list_ollama_models( - _: Any = Depends(current_user), + server: Any = Depends(get_server), + _: Any = Depends(require_permission("ollama_models")), ) -> list[OllamaModelResponse]: """Return the current Ollama model list via the SDK.""" + if not _ollama_service_enabled(server): + raise HTTPException( + status_code=503, + detail="Ollama service is disabled. Enable it from the provider card.", + ) try: - from octop.infra.ollama_manager import OllamaModelManager + from octop.infra.utils.ollama_manager import OllamaModelManager models = OllamaModelManager.list_models() except (OSError, ImportError) as exc: @@ -115,7 +121,7 @@ async def list_ollama_models( ) async def download_ollama_model( body: OllamaDownloadRequest, - _: Any = Depends(current_user), + _: Any = Depends(require_permission("ollama_models")), ) -> OllamaDownloadTaskResponse: """Start a background pull via Ollama SDK. @@ -146,7 +152,7 @@ async def _run_pull_in_background( loop: asyncio.AbstractEventLoop, ) -> None: """Execute the Ollama pull in a thread and update task status.""" - from octop.infra.ollama_manager import OllamaModelInfo, OllamaModelManager + from octop.infra.utils.ollama_manager import OllamaModelInfo, OllamaModelManager await update_status(task_id, DownloadTaskStatus.DOWNLOADING) @@ -176,7 +182,7 @@ async def _run_pull_in_background( summary="Get Ollama download tasks", ) async def get_ollama_download_status( - _: Any = Depends(current_user), + _: Any = Depends(require_permission("ollama_models")), ) -> list[OllamaDownloadTaskResponse]: """Return all Ollama-related download tasks.""" tasks = await get_tasks(backend="ollama") @@ -189,7 +195,7 @@ async def get_ollama_download_status( ) async def cancel_ollama_download( task_id: str, - _: Any = Depends(current_user), + _: Any = Depends(require_permission("ollama_models")), ) -> dict[str, Any]: """Cancel a pending or downloading Ollama model pull.""" success = await cancel_task(task_id) @@ -207,11 +213,11 @@ async def cancel_ollama_download( ) async def delete_ollama_model( name: str, - _: Any = Depends(current_user), + _: Any = Depends(require_permission("ollama_models")), ) -> dict[str, Any]: """Delete an Ollama model via the SDK.""" try: - from octop.infra.ollama_manager import OllamaModelManager + from octop.infra.utils.ollama_manager import OllamaModelManager except (OSError, ImportError) as exc: raise HTTPException( status_code=503, @@ -225,3 +231,75 @@ async def delete_ollama_model( raise HTTPException(status_code=400, detail=str(exc)) from exc return {"status": "deleted", "name": name} + + +_SETTINGS_KEY_OLLAMA_SERVICE = "ollama_service_enabled" + + +class OllamaServiceBody(BaseModel): + enabled: bool = Field(..., description="Whether Octop should keep the Ollama service running") + + +class OllamaServiceStatus(BaseModel): + enabled: bool + running: bool + + +def _ollama_service_enabled(server: Any) -> bool: + raw = server.services.settings_repo.get(_SETTINGS_KEY_OLLAMA_SERVICE) + if raw is None: + return False + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +@router.get( + "/service", + response_model=OllamaServiceStatus, + summary="Get Ollama local service status", +) +async def get_ollama_service( + server: Any = Depends(get_server), + _: Any = Depends(require_permission("ollama_models")), +) -> OllamaServiceStatus: + from octop.infra.utils.ollama_manager import is_ollama_reachable + + enabled = _ollama_service_enabled(server) + running = False + try: + running = is_ollama_reachable() + except Exception: + running = False + return OllamaServiceStatus(enabled=enabled, running=running) + + +@router.put( + "/service", + response_model=OllamaServiceStatus, + summary="Enable/disable Ollama local service", +) +async def put_ollama_service( + body: OllamaServiceBody, + server: Any = Depends(get_server), + _: Any = Depends(require_permission("ollama_models")), +) -> OllamaServiceStatus: + from octop.infra.utils.ollama_manager import ( + is_ollama_reachable, + start_ollama_service, + stop_ollama_service, + ) + + server.services.settings_repo.set( + _SETTINGS_KEY_OLLAMA_SERVICE, + "true" if body.enabled else "false", + ) + if body.enabled: + try: + start_ollama_service() + except Exception as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + else: + stop_ollama_service() + return OllamaServiceStatus( + enabled=body.enabled, + running=is_ollama_reachable(), + ) diff --git a/src/octop/api/routers/onnx_models.py b/src/octop/api/routers/onnx_models.py new file mode 100644 index 00000000..392d1596 --- /dev/null +++ b/src/octop/api/routers/onnx_models.py @@ -0,0 +1,234 @@ +"""Local ONNX embedding model cache API (Models admin — local tab). + +Manages catalog download / probe / enable for local ONNX embedding weights. +This is not a chat Provider. Knowledge Bases consume these models for embedding. +""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field + +from octop.api.deps import get_server, require_permission +from octop.infra.agents.providers.onnx_catalog import list_onnx_catalog_models +from octop.infra.agents.providers.onnx_service import ( + DOWNLOAD_MANAGER, + OnnxServiceConfig, + assert_catalog_model, + delete_downloaded_model, + embed_texts, + ensure_local_embedding_deps_async, + is_model_downloaded, + save_config, + status_payload, +) + +router = APIRouter(prefix="/onnx-models", tags=["onnx"]) + + +class OnnxCatalogItem(BaseModel): + id: str + name: str + recommended: bool = False + downloaded: bool = False + size_gb: float | None = None + hf_source: str | None = None + + +class OnnxServiceConfigBody(BaseModel): + enabled: bool = Field(..., description="Whether the local ONNX service is enabled") + model: str = Field(..., min_length=1, description="Selected catalog model id") + download_if_missing: bool = Field( + True, + description="When enabling, start a download if the model is not cached", + ) + + +class OnnxDownloadRequest(BaseModel): + model: str = Field(..., min_length=1, description="Catalog model id to download") + + +class OnnxTestRequest(BaseModel): + model: str = Field(..., min_length=1, description="Downloaded catalog model id to probe") + + +class OnnxTestResponse(BaseModel): + ok: bool + latency_ms: float | None = None + error: str | None = None + dim: int | None = None + + +def _settings_get(server: Any) -> Any: + return server.services.settings_repo.get + + +def _settings_set(server: Any) -> Any: + return server.services.settings_repo.set + + +def _http_from_value_error(exc: ValueError) -> HTTPException: + return HTTPException(status_code=400, detail=str(exc)) + + +def _http_from_runtime(exc: RuntimeError) -> HTTPException: + detail = str(exc) + code = 503 if "local-embedding" in detail or "fastembed" in detail else 409 + return HTTPException(status_code=code, detail=detail) + + +@router.get("/catalog", response_model=list[OnnxCatalogItem], summary="List ONNX catalog") +async def get_catalog(_: Any = Depends(require_permission("onnx_models"))) -> list[OnnxCatalogItem]: + return [ + OnnxCatalogItem( + id=str(m["id"]), + name=str(m.get("name") or m["id"]), + recommended=bool(m.get("recommended")), + downloaded=is_model_downloaded(str(m["id"])), + size_gb=float(m["size_gb"]) if m.get("size_gb") is not None else None, + hf_source=str(m["hf_source"]) if m.get("hf_source") else None, + ) + for m in list_onnx_catalog_models() + ] + + +@router.get("/models/{model_name:path}/meta", summary="ONNX model size / source metadata") +async def get_model_meta( + model_name: str, + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + from octop.infra.agents.providers.onnx_catalog import get_onnx_model_meta + + try: + name = assert_catalog_model(model_name) + except ValueError as exc: + raise _http_from_value_error(exc) from exc + meta = get_onnx_model_meta(name) + meta["downloaded"] = is_model_downloaded(name) + return meta + + +@router.get("/status", summary="Get local ONNX service status") +async def get_status( + server: Any = Depends(get_server), + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + return status_payload(_settings_get(server), DOWNLOAD_MANAGER.state) + + +@router.put("/config", summary="Enable/disable local ONNX service and select model") +async def put_config( + body: OnnxServiceConfigBody, + server: Any = Depends(get_server), + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + try: + model = assert_catalog_model(body.model) + except ValueError as exc: + raise _http_from_value_error(exc) from exc + config = OnnxServiceConfig(enabled=body.enabled, model=model) + deps_just_installed = False + if config.enabled: + try: + deps_just_installed = ( + await ensure_local_embedding_deps_async(allow_install=True) == "installed" + ) + except RuntimeError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + save_config(_settings_set(server), config) + download_started = False + if config.enabled and body.download_if_missing and not is_model_downloaded(config.model): + try: + await DOWNLOAD_MANAGER.start_download(config.model) + download_started = True + except RuntimeError as exc: + raise _http_from_runtime(exc) from exc + except ValueError as exc: + raise _http_from_value_error(exc) from exc + payload = status_payload(_settings_get(server), DOWNLOAD_MANAGER.state) + payload["download_started"] = download_started + payload["deps_just_installed"] = deps_just_installed + return payload + + +@router.post( + "/test", + response_model=OnnxTestResponse, + summary="Probe a local ONNX embedding model", +) +async def post_test( + body: OnnxTestRequest, + _: Any = Depends(require_permission("onnx_models")), +) -> OnnxTestResponse: + """Run a tiny local embedding to verify the model loads and works (admin only).""" + try: + model = assert_catalog_model(body.model) + await ensure_local_embedding_deps_async(allow_install=True) + except ValueError as exc: + return OnnxTestResponse(ok=False, error=str(exc)) + except RuntimeError as exc: + return OnnxTestResponse(ok=False, error=str(exc)) + if not is_model_downloaded(model): + return OnnxTestResponse( + ok=False, + error="model is not downloaded yet; download it before testing", + ) + + import asyncio + import time + + def _probe() -> int: + vectors = embed_texts(model, ["octop onnx probe"]) + if not vectors: + raise RuntimeError("embedding returned no vectors") + return len(vectors[0]) + + started = time.perf_counter() + try: + loop = asyncio.get_running_loop() + dim = await loop.run_in_executor(None, _probe) + except Exception as exc: + return OnnxTestResponse(ok=False, error=str(exc)) + latency_ms = (time.perf_counter() - started) * 1000.0 + return OnnxTestResponse(ok=True, latency_ms=latency_ms, dim=dim) + + +@router.post("/download", summary="Download a local ONNX embedding model") +async def post_download( + body: OnnxDownloadRequest, + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + try: + state = await DOWNLOAD_MANAGER.start_download(body.model.strip()) + except RuntimeError as exc: + raise _http_from_runtime(exc) from exc + except ValueError as exc: + raise _http_from_value_error(exc) from exc + return state.to_dict() + + +@router.get("/download-status", summary="Poll ONNX model download progress") +async def get_download_status( + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + return DOWNLOAD_MANAGER.state.to_dict() + + +@router.delete("/local/{model_name:path}", summary="Delete a cached local ONNX model") +async def delete_local_model( + model_name: str, + server: Any = Depends(get_server), + _: Any = Depends(require_permission("onnx_models")), +) -> dict[str, Any]: + try: + name = assert_catalog_model(model_name) + except ValueError as exc: + raise _http_from_value_error(exc) from exc + removed = delete_downloaded_model(name) + return { + "ok": True, + "removed": removed, + "status": status_payload(_settings_get(server), DOWNLOAD_MANAGER.state), + } diff --git a/src/octop/api/routers/plugins.py b/src/octop/api/routers/plugins.py index 28ea99f5..b49f2dc2 100644 --- a/src/octop/api/routers/plugins.py +++ b/src/octop/api/routers/plugins.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, Field from octop.api.common.agent import assert_agent_owner as _assert_agent_owner -from octop.api.deps import current_user, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.agents.plugins.manager import PluginManager from octop.infra.errors import ErrorCode, OctopError from octop.infra.server import OctopServer @@ -41,11 +41,6 @@ def _plugin_manager(server: OctopServer) -> PluginManager: return mgr -def _require_admin(user: Any) -> None: - if not getattr(user, "is_admin", False): - raise OctopError(ErrorCode.FORBIDDEN, "admin only") - - @router.get("", summary="List installed plugins") async def list_plugins( server: OctopServer = Depends(get_server), @@ -59,9 +54,8 @@ async def list_plugins( async def install_plugin( body: PluginInstallBody, server: OctopServer = Depends(get_server), - user: Any = Depends(current_user), + _user: Any = Depends(require_permission("plugins")), ) -> dict[str, Any]: - _require_admin(user) mgr = _plugin_manager(server) try: loaded = mgr.install_url(body.url) @@ -89,13 +83,12 @@ async def upload_plugin( file: UploadFile = File(...), force: bool = Form(default=False), server: OctopServer = Depends(get_server), - user: Any = Depends(current_user), + _user: Any = Depends(require_permission("plugins")), ) -> dict[str, Any]: """Install a plugin from a locally-uploaded ZIP archive. ``force=True`` overwrites an already-installed plugin with the same id. """ - _require_admin(user) mgr = _plugin_manager(server) raw = await file.read() if not raw: @@ -131,9 +124,8 @@ async def upload_plugin( async def uninstall_plugin( plugin_id: str, server: OctopServer = Depends(get_server), - user: Any = Depends(current_user), + _user: Any = Depends(require_permission("plugins")), ) -> dict[str, str]: - _require_admin(user) _plugin_manager(server).uninstall(plugin_id) if server.app_runtime is not None: await server.app_runtime.agent_registry.reload_all() diff --git a/src/octop/api/routers/proactive_care.py b/src/octop/api/routers/proactive_care.py index 43d83a8d..35b459ba 100644 --- a/src/octop/api/routers/proactive_care.py +++ b/src/octop/api/routers/proactive_care.py @@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel, field_validator, model_validator -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.deps import current_user, get_server from octop.infra.db.repos.proactive_care_config import ProactiveCareConfig @@ -104,7 +104,7 @@ async def get_proactive_care_config( server: Any = Depends(get_server), ) -> dict[str, Any]: """Read the agent's proactive care push configuration, returning defaults if not configured.""" - require_agent_row(agent_id, user=user, as_user=None, server=server) + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) cfg = server.services.repos.proactive_care_config_repo.get(agent_id) return _config_to_dict(cfg) @@ -126,7 +126,7 @@ async def put_proactive_care_config( - min_interval_hours <= max_interval_hours - min_interval_hours >= 1 """ - require_agent_row(agent_id, user=user, as_user=None, server=server) + require_agent_owner_row(agent_id, user=user, as_user=None, server=server) cfg = ProactiveCareConfig( agent_id=agent_id, diff --git a/src/octop/api/routers/providers.py b/src/octop/api/routers/providers.py index 592fb65a..e030f13f 100644 --- a/src/octop/api/routers/providers.py +++ b/src/octop/api/routers/providers.py @@ -11,7 +11,8 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel -from octop.api.deps import current_admin, current_user, get_server +from octop.api.deps import current_user, get_server, require_permission +from octop.infra.agents.providers.model_flags import is_local_runtime_provider from octop.infra.agents.providers.presets import load_provider_presets from octop.infra.agents.providers.probe import ( fetch_openai_compatible_models, @@ -75,6 +76,7 @@ def _patch_requires_provider_rehydrate(body: ProviderPatchBody) -> bool: class ProviderTestBody(BaseModel): model_id: str | None = None + embedding: bool = False class ProviderTestDraftBody(BaseModel): @@ -84,6 +86,7 @@ class ProviderTestDraftBody(BaseModel): base_url: str | None = None model_id: str extra_json: str | None = None + embedding: bool = False class ProviderFetchModelsBody(BaseModel): @@ -170,7 +173,7 @@ async def get_active_model( @router.put("/active-model") async def set_active_model( body: ActiveModelBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, str]: """Set the globally preferred model used when no agent override applies.""" @@ -196,7 +199,7 @@ async def list_providers( @admin_router.get("") async def admin_list_providers( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: return [_row_to_dict(r) for r in server.services.provider_repo.list_all()] @@ -205,7 +208,7 @@ async def admin_list_providers( @admin_router.post("", status_code=201) async def admin_create_provider( body: ProviderCreateBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, Any]: import json as _json @@ -229,7 +232,7 @@ async def admin_create_provider( async def admin_patch_provider( provider_id: int, body: ProviderPatchBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, Any]: row = server.services.provider_repo.get(provider_id) @@ -263,12 +266,21 @@ async def admin_patch_provider( @admin_router.delete("/{provider_id}", status_code=204) async def admin_delete_provider( provider_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> None: row = server.services.provider_repo.get(provider_id) if row is None: raise OctopError(ErrorCode.NOT_FOUND, "provider not found") + if is_local_runtime_provider( + row.name, + provider_api_key=row.api_key, + provider_base_url=row.base_url, + ): + raise OctopError( + ErrorCode.PROVIDER_LOCAL_PROTECTED, + "local runtime providers cannot be deleted", + ) refs = server.app_runtime.agent_registry.find_agents_using_provider(row.name) if refs: raise OctopError( @@ -285,7 +297,7 @@ async def admin_delete_provider( @admin_router.post("/test-draft", summary="Test unsaved provider draft") async def admin_test_provider_draft( body: ProviderTestDraftBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), ) -> dict[str, Any]: """Probe connectivity for a provider draft before it is saved.""" api_key = (body.api_key or "").strip() @@ -301,14 +313,15 @@ async def admin_test_provider_draft( base_url=(body.base_url or "").strip() or None, model_id=model_id, extra_json=body.extra_json, + embedding=body.embedding, ) - return await probe_provider_row(row, model_id=model_id) + return await probe_provider_row(row, model_id=model_id, embedding=body.embedding) @admin_router.post("/fetch-models", summary="List models from an OpenAI-compatible draft") async def admin_fetch_provider_models( body: ProviderFetchModelsBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), ) -> dict[str, Any]: """Fetch remote model ids via OpenAI-compatible ``GET /models`` (openai kind only).""" if body.kind != "openai": @@ -378,7 +391,7 @@ async def _run_codex_device_poll( @admin_router.post("/codex-oauth/start", summary="Start ChatGPT OAuth device login") async def codex_oauth_start( - user: Any = Depends(current_admin), + user: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, Any]: info = await asyncio.to_thread(request_device_code) @@ -407,7 +420,7 @@ async def codex_oauth_start( @admin_router.get("/codex-oauth/pending/{state_id}", summary="Poll ChatGPT OAuth result") async def codex_oauth_pending( state_id: str, - user: Any = Depends(current_admin), + user: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, Any]: raw = server.services.settings_repo.get(f"codex_oauth.pending.{state_id}") @@ -425,7 +438,7 @@ async def codex_oauth_pending( @admin_router.delete("/codex-oauth", status_code=204, summary="Clear ChatGPT OAuth login") async def codex_oauth_logout( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> None: from octop.infra.providers.codex_oauth import delete_codex_token @@ -444,13 +457,14 @@ async def codex_oauth_logout( async def admin_test_provider( provider_id: int, body: ProviderTestBody | None = None, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("providers")), server: Any = Depends(get_server), ) -> dict[str, Any]: - """Probe a provider by sending a one-token ping and timing it.""" + """Probe a provider: chat ping, or ``POST /embeddings`` for embedding models.""" row = server.services.provider_repo.get(provider_id) if row is None: raise OctopError(ErrorCode.NOT_FOUND, "provider not found") row = await _maybe_refresh_codex_row(server, row) model_id = body.model_id if body else None - return await probe_provider_row(row, model_id=model_id) + embedding = body.embedding if body else None + return await probe_provider_row(row, model_id=model_id, embedding=embedding) diff --git a/src/octop/api/routers/search.py b/src/octop/api/routers/search.py index 1a8711b7..87044d17 100644 --- a/src/octop/api/routers/search.py +++ b/src/octop/api/routers/search.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel, Field -from octop.api.deps import current_admin +from octop.api.deps import require_permission from octop.infra.utils.search_probe import probe_search_provider router = APIRouter(prefix="/search", tags=["search"]) @@ -46,7 +46,7 @@ class TestSearchResponse(BaseModel): async def test_search_provider( provider_id: str, body: TestSearchRequest, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("search")), ) -> TestSearchResponse: result = await probe_search_provider(provider_id, body.env_vars) return TestSearchResponse(**result) diff --git a/src/octop/api/routers/security.py b/src/octop/api/routers/security.py index 983e635f..bccc3a14 100644 --- a/src/octop/api/routers/security.py +++ b/src/octop/api/routers/security.py @@ -8,7 +8,8 @@ from harness_agent.security.models import DEFAULT_HITL_TOOLS, SecurityPolicy from pydantic import BaseModel, Field -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission +from octop.i18n.domains.tools import hitl_tool_catalog from octop.infra.db.repos.audit import ACTOR_ADMIN router = APIRouter() @@ -43,7 +44,7 @@ def _to_response(policy: SecurityPolicy) -> SecurityPolicyResponse: @router.get("", summary="Get global security policy") async def get_security_policy( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> SecurityPolicyResponse: return _to_response(server.app_runtime.agent_registry.security.load()) @@ -52,7 +53,7 @@ async def get_security_policy( @router.put("", summary="Update global security policy") async def put_security_policy( body: SecurityPolicyBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> SecurityPolicyResponse: current = server.app_runtime.agent_registry.security.load().to_dict() @@ -108,8 +109,15 @@ class ToolGuardRulesSaveResponse(BaseModel): rule_count: int +class HitlToolCatalogItem(BaseModel): + name: str + label_zh: str + label_en: str + + class SecurityDefaultsResponse(BaseModel): hitl_tools: list[str] + hitl_tool_catalog: list[HitlToolCatalogItem] tool_guard_rules: list[ToolGuardRuleItem] @@ -119,7 +127,7 @@ def _rules_store(server: Any) -> Any: @router.get("/tool-guard/rules", summary="List active command guard rules") async def get_tool_guard_rules( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> ToolGuardRulesResponse: store = _rules_store(server) @@ -134,7 +142,7 @@ async def get_tool_guard_rules( @router.get("/tool-guard/rules/raw", summary="Get editable command guard rules YAML") async def get_tool_guard_rules_raw( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> ToolGuardRulesRawResponse: store = _rules_store(server) @@ -144,7 +152,7 @@ async def get_tool_guard_rules_raw( @router.put("/tool-guard/rules/raw", summary="Save command guard rules YAML") async def put_tool_guard_rules_raw( body: ToolGuardRulesRawBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> ToolGuardRulesSaveResponse: store = _rules_store(server) @@ -164,7 +172,7 @@ async def put_tool_guard_rules_raw( @router.post("/tool-guard/rules/reset", summary="Reset command guard rules to shipped defaults") async def post_tool_guard_rules_reset( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> ToolGuardRulesRawResponse: store = _rules_store(server) @@ -181,12 +189,16 @@ async def post_tool_guard_rules_reset( @router.get("/defaults", summary="Security defaults and active rule catalogs") async def get_security_defaults( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("security")), server: Any = Depends(get_server), ) -> SecurityDefaultsResponse: store = _rules_store(server) raw = store.list_catalog() return SecurityDefaultsResponse( hitl_tools=list(DEFAULT_HITL_TOOLS), + hitl_tool_catalog=[ + HitlToolCatalogItem(name=e.name, label_zh=e.label_zh, label_en=e.label_en) + for e in hitl_tool_catalog() + ], tool_guard_rules=[ToolGuardRuleItem(**item) for item in raw], ) diff --git a/src/octop/api/routers/skill_packages.py b/src/octop/api/routers/skill_packages.py index 28981c82..d6faa9c0 100644 --- a/src/octop/api/routers/skill_packages.py +++ b/src/octop/api/routers/skill_packages.py @@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends, Request, status from pydantic import BaseModel -from octop.api.deps import current_user, get_server +from octop.api.deps import get_server, require_permission from octop.infra.agents.experts.skillhub_market import ( SkillHubMarketError, SkillHubMarketErrorKind, @@ -224,7 +224,7 @@ def _package_skill_or_404( @router.get("", summary="List global skill packages") async def list_skill_packages( server: OctopServer = Depends(get_server), - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> list[dict[str, Any]]: store = _store(server) return [ @@ -236,7 +236,7 @@ async def list_skill_packages( async def package_hub_search( q: str = "", limit: int = 50, - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> list[dict[str, Any]]: from fastapi import HTTPException # noqa: PLC0415 @@ -256,7 +256,7 @@ async def package_hub_search( @router.get("/hub/rankings", summary="SkillHub rankings for installing into packages") async def package_hub_rankings( type: str = "all", - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: from fastapi import HTTPException # noqa: PLC0415 @@ -280,7 +280,7 @@ async def package_hub_rankings( async def create_skill_package( body: CreateSkillPackageBody, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) name = _required(body.name, "name") @@ -307,7 +307,7 @@ async def create_skill_package( async def create_from_skillhub( body: FromSkillHubBody, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) try: @@ -331,7 +331,7 @@ async def get_skill_package( package_id: str, request: Request, server: OctopServer = Depends(get_server), - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -344,7 +344,7 @@ async def update_skill_package( body: UpdateSkillPackageBody, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -376,7 +376,7 @@ async def delete_skill_package( package_id: str, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> None: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -391,7 +391,7 @@ async def list_package_skills( package_id: str, request: Request, server: OctopServer = Depends(get_server), - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> list[dict[str, Any]]: store = _store(server) _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -404,7 +404,7 @@ async def get_package_skill( slug: str, request: Request, server: OctopServer = Depends(get_server), - _user: User = Depends(current_user), + _user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -420,7 +420,7 @@ async def create_package_skill( body: CreatePackageSkillBody, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -449,7 +449,7 @@ async def update_package_skill( body: UpdatePackageSkillBody, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -475,7 +475,7 @@ async def delete_package_skill( slug: str, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> None: store = _store(server) row = _package_or_404(store, package_id, locale=resolve_request_locale(request)) @@ -530,7 +530,7 @@ async def import_package_skill( body: ImportPackageSkillBody, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: import asyncio # noqa: PLC0415 from urllib.error import HTTPError, URLError # noqa: PLC0415 @@ -610,7 +610,7 @@ async def hub_install_package_skill( body: HubInstallPackageSkillBody, request: Request, server: OctopServer = Depends(get_server), - user: User = Depends(current_user), + user: User = Depends(require_permission("skill_packages")), ) -> dict[str, Any]: from fastapi import HTTPException # noqa: PLC0415 diff --git a/src/octop/api/routers/skills.py b/src/octop/api/routers/skills.py index d44da20f..370860af 100644 --- a/src/octop/api/routers/skills.py +++ b/src/octop/api/routers/skills.py @@ -44,7 +44,7 @@ from fastapi import APIRouter, Depends, Request from pydantic import BaseModel -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.deps import current_user, get_server from octop.infra.agents.manager import ( skill_package_ids_list, @@ -91,7 +91,7 @@ async def _ctx( ) -> _AgentCtx: assert server.app_runtime is not None registry = server.app_runtime.agent_registry - row = require_agent_row(agent_id, user=user, as_user=as_user, server=server) + row = require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) cfg = registry.get_config(agent_id) agent = registry.get_agent(agent_id) return _AgentCtx(runtime=row, workspace=agent.workspace, config=cfg) @@ -502,7 +502,7 @@ async def list_skill_package_mounts( user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> dict[str, Any]: - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) assert server.app_runtime is not None package_ids = skill_package_ids_list(server.app_runtime.agent_registry.get_config(agent_id)) assert server.services is not None @@ -537,7 +537,7 @@ async def replace_skill_package_mounts( user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> dict[str, list[str]]: - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) assert server.app_runtime is not None package_ids = skill_package_ids_list({"skill_package_ids": body.package_ids}) await server.app_runtime.agent_registry.persist_skill_package_ids(agent_id, package_ids) @@ -888,6 +888,31 @@ async def _persist_disabled(server: Any, agent_id: str, disabled: set[str]) -> N await server.app_runtime.agent_registry.persist_skills_disabled(agent_id, disabled) +async def _skill_disable_keys(ctx: _AgentCtx, name: str) -> set[str]: + """Return slug / display-name keys to toggle for enable/disable.""" + from harness_agent.skills.catalog import skill_identity_keys + + slug = name.strip() + keys = {slug} if slug else set() + resolved = await _resolve_skill(ctx.workspace, slug) + if resolved is None: + return keys + manifest_path, kind, _body = resolved + manifest = await _aread_text(ctx.workspace, manifest_path) + if manifest is None: + return keys + meta, _ = _parse_frontmatter(manifest) + summary = _summary_dict(slug, meta, enabled=False, kind=kind) + keys |= skill_identity_keys( + { + "name": summary["name"], + "slug": summary["slug"], + "path": manifest_path, + } + ) + return keys + + @router.post("/agents/{agent_id}/skills/{name}/enable", status_code=204) async def enable_skill( agent_id: str, @@ -898,8 +923,9 @@ async def enable_skill( ) -> None: ctx = await _ctx(agent_id, user=user, as_user=as_user, server=server) disabled = _disabled_set(ctx.config) - if name in disabled: - disabled.discard(name) + keys = await _skill_disable_keys(ctx, name) + if disabled & keys: + disabled -= keys await _persist_disabled(server, agent_id, disabled) @@ -913,7 +939,7 @@ async def disable_skill( ) -> None: ctx = await _ctx(agent_id, user=user, as_user=as_user, server=server) disabled = _disabled_set(ctx.config) - disabled.add(name) + disabled |= await _skill_disable_keys(ctx, name) await _persist_disabled(server, agent_id, disabled) @@ -1082,7 +1108,7 @@ async def hub_search_skills( # Verify the agent exists and belongs to this user. The skillhub CLI # runs globally, so we only need an existence/ownership check here — # the agent need not be running (unlike chat/workspace endpoints). - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) locale = resolve_request_locale(request) query = q.strip() or "a" effective_limit = max(1, min(limit, 100)) @@ -1139,7 +1165,7 @@ async def hub_rankings( """ from fastapi import HTTPException # noqa: PLC0415 - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) rtype = type if type in _RANKING_TYPES else "all" from octop.infra.skills.skillhub_market import ( # noqa: PLC0415 diff --git a/src/octop/api/routers/storage_backends.py b/src/octop/api/routers/storage_backends.py index 76768200..119573d3 100644 --- a/src/octop/api/routers/storage_backends.py +++ b/src/octop/api/routers/storage_backends.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel -from octop.api.deps import current_admin, current_user, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.backend.adapter import row_to_backend_spec from octop.infra.backend.docker_spec import docker_spec_previewable from octop.infra.errors import ErrorCode, OctopError @@ -79,7 +79,7 @@ def _row_to_dict(r: Any) -> dict[str, Any]: @admin_router.get("") async def list_storage_backends( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: return [_row_to_dict(r) for r in server.services.storage_backend_repo.list_all()] @@ -88,7 +88,7 @@ async def list_storage_backends( @admin_router.post("", status_code=201) async def create_storage_backend( body: StorageBackendCreateBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> dict[str, Any]: if server.services.storage_backend_repo.get_by_name(body.name) is not None: @@ -111,7 +111,7 @@ async def create_storage_backend( async def patch_storage_backend( backend_id: int, body: StorageBackendPatchBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> dict[str, Any]: row = server.services.storage_backend_repo.get(backend_id) @@ -135,7 +135,7 @@ async def patch_storage_backend( @admin_router.delete("/{backend_id}", status_code=204) async def delete_storage_backend( backend_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> None: row = server.services.storage_backend_repo.get(backend_id) @@ -155,7 +155,7 @@ async def delete_storage_backend( @admin_router.post("/probe") async def probe_storage_backend_config( body: StorageBackendProbeBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Probe connectivity from unsaved form values (optional merge with stored row).""" @@ -183,7 +183,7 @@ async def probe_storage_backend_config( async def list_storage_backend_tree( backend_id: int, path: str = "/", - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: """Single-level directory listing for a configured storage backend.""" @@ -201,7 +201,7 @@ async def list_storage_backend_tree( @admin_router.post("/{backend_id}/test") async def test_storage_backend( backend_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("storage_backends")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Best-effort connectivity / configuration probe for one backend.""" diff --git a/src/octop/api/routers/subagents.py b/src/octop/api/routers/subagents.py index 2067d7a9..fe9ff99d 100644 --- a/src/octop/api/routers/subagents.py +++ b/src/octop/api/routers/subagents.py @@ -26,7 +26,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel, Field -from octop.api.common.agent import require_agent_row +from octop.api.common.agent import require_agent_owner_row from octop.api.common.workspace import require_running_workspace from octop.api.deps import current_user, get_server from octop.infra.agents.subagents.catalog import SubagentDefinition @@ -159,7 +159,7 @@ async def list_subagents( user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: - require_agent_row(agent_id, user=user, as_user=as_user, server=server) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) assert server.app_runtime is not None registry = server.app_runtime.agent_registry return cast(list[dict[str, Any]], await registry.list_subagent_summaries(agent_id)) @@ -190,6 +190,7 @@ async def install_subagent( detail="slug is required and must not contain / or start with .", ) + require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server) catalog = _require_catalog(server) item = catalog.get(slug) if item is None: @@ -225,6 +226,7 @@ async def install_subagent( user=user, as_user=as_user, server=server, + owner_only=True, ) dest = f"agents/{slug}.md" await workspace.aupload_bytes(dest, content.encode("utf-8")) diff --git a/src/octop/api/routers/terminal.py b/src/octop/api/routers/terminal.py index 81775d5f..014a563e 100644 --- a/src/octop/api/routers/terminal.py +++ b/src/octop/api/routers/terminal.py @@ -65,9 +65,11 @@ from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect from starlette.websockets import WebSocketState +from octop.api.common.agent import assert_agent_owner from octop.api.common.agent_workspace import resolve_agent_workspace_dir -from octop.api.deps import current_user, get_server, resolve_user_from_token +from octop.api.deps import get_server, require_permission, resolve_user_from_token from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.permissions import user_has_permission from octop.infra.utils import posix_compat logger = logging.getLogger(__name__) @@ -443,7 +445,7 @@ def _reap() -> None: @router.get("/agents/{agent_id}/terminal/context") async def terminal_context( agent_id: str, - user: Any = Depends(current_user), + user: Any = Depends(require_permission("terminal")), server: Any = Depends(get_server), ) -> dict[str, Any]: """Return system context for the AI terminal panel.""" @@ -451,6 +453,7 @@ async def terminal_context( row = server.app_runtime.agent_registry.get_row(agent_id) if row is None: raise OctopError(ErrorCode.AGENT_NOT_FOUND, "no agents for user") + assert_agent_owner(row, user) workspace_dir = str(resolve_agent_workspace_dir(server, agent_id)) os_name = platform.system() @@ -512,6 +515,9 @@ async def terminal_ws( except OctopError as exc: await websocket.close(code=4001, reason=f"auth: {exc.code.value}") return + if not user_has_permission(user, "terminal"): + await websocket.close(code=4003, reason="permission required") + return assert server.app_runtime is not None registry = server.app_runtime.agent_registry agent_row = registry.get_row(agent_id) @@ -535,6 +541,11 @@ async def terminal_ws( if agent_row is None: await websocket.close(code=4404, reason="agent not found") return + try: + assert_agent_owner(agent_row, user) + except OctopError: + await websocket.close(code=4003, reason="agent not owned by user") + return workspace_dir = str(resolve_agent_workspace_dir(server, agent_id)) supported, unsupported_reason = terminal_supported() diff --git a/src/octop/api/routers/tls.py b/src/octop/api/routers/tls.py index 2993b890..e0dee075 100644 --- a/src/octop/api/routers/tls.py +++ b/src/octop/api/routers/tls.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field -from octop.api.deps import current_admin, get_server +from octop.api.deps import get_server, require_permission from octop.infra.errors import ErrorCode, OctopError from octop.infra.setup.tls.manager import get_tls_manager from octop.infra.setup.tls.modes import is_issue_eligible, validate_issue_domain @@ -54,7 +54,7 @@ def _preflight_response(result: PreflightResult) -> PreflightResponse: @router.get("/status", summary="TLS and issuance task status") async def tls_status( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("tls")), server: Any = Depends(get_server), ) -> TlsStatusResponse: assert server.services is not None @@ -67,7 +67,7 @@ async def tls_status( async def tls_preflight( body: PreflightBody, request: Request, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("tls")), server: Any = Depends(get_server), ) -> PreflightResponse: assert server.services is not None @@ -81,7 +81,7 @@ async def tls_preflight( async def tls_issue( body: IssueBody, request: Request, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("tls")), server: Any = Depends(get_server), ) -> PreflightResponse: assert server.services is not None diff --git a/src/octop/api/routers/update.py b/src/octop/api/routers/update.py index d15ad63a..3f49f4a2 100644 --- a/src/octop/api/routers/update.py +++ b/src/octop/api/routers/update.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, BackgroundTasks, Depends, Query -from octop.api.deps import current_admin, current_user +from octop.api.deps import current_user, require_permission from octop.api.routers.update_store import ( UpgradeTaskStatus, cache_status, @@ -68,19 +68,22 @@ def _build_status( @router.get("/status") async def update_status(_: Any = Depends(current_user)) -> dict[str, Any]: + """Return last check result; re-probe PyPI when the server cache TTL expires.""" cached = get_cached_status() if cached is not None: return cached - return _build_status() + return await asyncio.to_thread(_build_status) @router.post("/check") -async def check_for_updates(_: Any = Depends(current_admin)) -> dict[str, Any]: +async def check_for_updates(_: Any = Depends(require_permission("update"))) -> dict[str, Any]: pypi_info = await asyncio.to_thread(fetch_pypi_info) if pypi_info is None: - return _build_status(latest=None, error="could not reach PyPI") + return await asyncio.to_thread(_build_status, latest=None, error="could not reach PyPI") release_notes = parse_changelog_for_version(pypi_info.description, pypi_info.version) - return _build_status(latest=pypi_info.version, release_notes=release_notes) + return await asyncio.to_thread( + _build_status, latest=pypi_info.version, release_notes=release_notes + ) async def _upgrade_worker(task_id: str) -> None: @@ -112,7 +115,7 @@ async def _upgrade_worker(task_id: str) -> None: @router.post("/upgrade") -async def trigger_upgrade(_: Any = Depends(current_admin)) -> dict[str, Any]: +async def trigger_upgrade(_: Any = Depends(require_permission("update"))) -> dict[str, Any]: if get_editable_path() is not None: raise OctopError( ErrorCode.FORBIDDEN, @@ -126,7 +129,7 @@ async def trigger_upgrade(_: Any = Depends(current_admin)) -> dict[str, Any]: @router.get("/progress") async def upgrade_progress( task_id: str = Query(...), - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("update")), ) -> dict[str, Any]: task = await get_task(task_id) if task is None: @@ -153,7 +156,7 @@ def _restart_service_task(runtime: ServiceRuntime) -> None: @router.post("/restart") async def restart_service_endpoint( background_tasks: BackgroundTasks, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("update")), ) -> dict[str, Any]: mode = detect_service_mode() if mode is None: diff --git a/src/octop/api/routers/update_store.py b/src/octop/api/routers/update_store.py index 6b96f2e1..154dfb15 100644 --- a/src/octop/api/routers/update_store.py +++ b/src/octop/api/routers/update_store.py @@ -10,6 +10,9 @@ from pydantic import BaseModel, Field +# Align with dashboard ``UPDATE_STATUS_TTL_MS`` (1 hour). +STATUS_CACHE_TTL_SECONDS = 60 * 60 + class UpgradeTaskStatus(StrEnum): RUNNING = "running" @@ -33,17 +36,30 @@ class UpgradeTask(BaseModel): _tasks: dict[str, UpgradeTask] = {} _lock = asyncio.Lock() _last_status: dict[str, Any] | None = None +_last_status_at: float | None = None -def cache_status(payload: dict[str, Any]) -> None: - global _last_status +def cache_status(payload: dict[str, Any], *, cached_at: float | None = None) -> None: + global _last_status, _last_status_at _last_status = payload + _last_status_at = time.time() if cached_at is None else cached_at -def get_cached_status() -> dict[str, Any] | None: +def get_cached_status(*, now: float | None = None) -> dict[str, Any] | None: + if _last_status is None or _last_status_at is None: + return None + t = time.time() if now is None else now + if t - _last_status_at >= STATUS_CACHE_TTL_SECONDS: + return None return _last_status +def clear_cached_status() -> None: + global _last_status, _last_status_at + _last_status = None + _last_status_at = None + + async def create_task() -> UpgradeTask: async with _lock: task = UpgradeTask() diff --git a/src/octop/api/routers/usage.py b/src/octop/api/routers/usage.py index 0bf625e8..46b00bff 100644 --- a/src/octop/api/routers/usage.py +++ b/src/octop/api/routers/usage.py @@ -16,7 +16,7 @@ from fastapi import APIRouter, Depends -from octop.api.deps import current_admin, current_user, get_server +from octop.api.deps import current_user, get_server from octop.infra.errors import ErrorCode, OctopError router = APIRouter() @@ -78,11 +78,13 @@ async def admin_summary( granularity: str = "by_day", user_id: int | None = None, agent_id: str | None = None, - _: Any = Depends(current_admin), + user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> dict[str, Any]: """Global usage roll-up. Optional ``user_id`` / ``agent_id`` narrow - the scope; absent both fields mean *all rows*.""" + the scope; absent both fields mean *all rows*. Admin only.""" + if not user.is_admin: + raise OctopError(ErrorCode.FORBIDDEN, "admin required") return cast( "dict[str, Any]", server.services.usage_repo.summary( diff --git a/src/octop/api/routers/users.py b/src/octop/api/routers/users.py index 339ed845..e35b9655 100644 --- a/src/octop/api/routers/users.py +++ b/src/octop/api/routers/users.py @@ -5,12 +5,14 @@ import time from typing import Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from pydantic import BaseModel, Field -from octop.api.deps import current_admin, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.errors import ErrorCode, OctopError -from octop.infra.users.identity import Role +from octop.infra.users.identity import Role, User +from octop.infra.users.permissions import PERMISSIONS +from octop.infra.utils.locale import resolve_request_locale router = APIRouter() @@ -20,12 +22,14 @@ class UserCreateBody(BaseModel): password: str = Field(min_length=1, max_length=200) role: str = "user" display_name: str | None = None + permissions: list[str] = Field(default_factory=list) class UserPatchBody(BaseModel): role: str | None = None display_name: str | None = None disabled: bool | None = None + permissions: list[str] | None = None class ResetPasswordBody(BaseModel): @@ -42,18 +46,89 @@ def _row_to_dict(r: Any) -> dict[str, Any]: "username": r.username, "role": r.role, "display_name": r.display_name, + "email": r.email, + "has_password": r.password_hash is not None, + "sso_linked": r.sso_provider_id is not None and r.sso_subject is not None, "disabled": bool(r.disabled), "login_failed_count": int(getattr(r, "login_failed_count", 0) or 0), "login_locked": locked, "login_locked_until": locked_until if locked else 0, "login_retry_after_seconds": retry_after, "created_at": int(r.created_at), + "permissions": list(getattr(r, "permissions", None) or []), } +def _assert_can_assign(actor: User, permissions: list[str]) -> None: + """Non-admin actors may only grant permissions they themselves hold.""" + if actor.is_admin: + return + missing = sorted(set(permissions) - set(actor.permissions or [])) + if missing: + raise OctopError( + ErrorCode.FORBIDDEN, + "cannot grant permissions you do not hold", + details={"missing": missing}, + ) + + +def _can_manage_users(row: Any) -> bool: + if str(getattr(row, "role", "")) == "admin": + return True + return "users" in (getattr(row, "permissions", None) or []) + + +def _assert_not_last_user_manager( + server: Any, + *, + actor: User, + target_user_id: int, + new_permissions: list[str], +) -> None: + """Refuse stripping ``users`` if no one else could manage users afterward.""" + if target_user_id != actor.id: + return + if "users" in new_permissions: + return + managers = [ + u + for u in server.user_manager.list_all(include_disabled=False) + if _can_manage_users(u) and int(u.id) != target_user_id + ] + if not managers: + raise OctopError( + ErrorCode.FORBIDDEN, + "cannot remove own user management permission", + ) + + +@router.get("/permissions", summary="List assignable permission catalog") +async def list_permission_catalog( + request: Request, + _: User = Depends(current_user), +) -> list[dict[str, str]]: + """Return all permissions, localized by Accept-Language, for the UI picker.""" + locale = resolve_request_locale(request) + items: list[dict[str, str]] = [] + zh = locale.startswith("zh") + for key, p in PERMISSIONS.items(): + labels = [(p.label_zh, p.label_en), *p.extra_tabs] + for label_zh, label_en in labels: + items.append( + { + "key": key, + "category": p.category, + "label": label_zh if zh else label_en, + "page": p.page, + "page_label": (p.page_zh if zh else p.page_en) if p.page else "", + } + ) + return items + + @router.get("") async def list_users( - _: Any = Depends(current_admin), server: Any = Depends(get_server) + _: Any = Depends(require_permission("users")), server: Any = Depends(get_server) ) -> list[dict[str, Any]]: rows = server.user_manager.list_all(include_disabled=True) return [_row_to_dict(r) for r in rows] @@ -62,15 +137,17 @@ async def list_users( @router.post("", status_code=201) async def create_user( body: UserCreateBody, - _: Any = Depends(current_admin), + actor: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> dict[str, Any]: + _assert_can_assign(actor, body.permissions) role = Role(body.role) user = await server.user_manager.create( username=body.username, password=body.password, role=role, display_name=body.display_name, + permissions=body.permissions, ) row = server.user_manager.get_row(user.id) assert row is not None @@ -80,7 +157,7 @@ async def create_user( @router.get("/{user_id}") async def get_user( user_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> dict[str, Any]: row = server.user_manager.get_row(user_id) @@ -93,14 +170,22 @@ async def get_user( async def patch_user( user_id: int, body: UserPatchBody, - admin: Any = Depends(current_admin), + actor: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> dict[str, Any]: row = server.user_manager.get_row(user_id) if row is None: raise OctopError(ErrorCode.NOT_FOUND, "user not found") + if body.permissions is not None: + _assert_can_assign(actor, body.permissions) + _assert_not_last_user_manager( + server, + actor=actor, + target_user_id=user_id, + new_permissions=body.permissions, + ) if body.role is not None: - if user_id == admin.id and Role(body.role) is not Role.ADMIN: + if user_id == actor.id and Role(body.role) is not Role.ADMIN: raise OctopError(ErrorCode.FORBIDDEN, "cannot demote yourself") await server.user_manager.set_role(row.username, Role(body.role)) if body.display_name is not None: @@ -109,6 +194,8 @@ async def patch_user( await server.user_manager.disable(row.username) elif body.disabled is False: await server.user_manager.enable(row.username) + if body.permissions is not None: + await server.user_manager.set_permissions(row.username, body.permissions) updated = server.user_manager.get_row(user_id) assert updated is not None return _row_to_dict(updated) @@ -117,10 +204,10 @@ async def patch_user( @router.post("/{user_id}/unlock-login", status_code=204, summary="Clear login lockout") async def unlock_user_login( user_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> None: - """Clear failed-login counter and temporary lock for a user (admin).""" + """Clear failed-login counter and temporary lock for a user.""" row = server.user_manager.get_row(user_id) if row is None: raise OctopError(ErrorCode.NOT_FOUND, "user not found") @@ -131,7 +218,7 @@ async def unlock_user_login( async def reset_password( user_id: int, body: ResetPasswordBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> None: row = server.user_manager.get_row(user_id) @@ -143,10 +230,10 @@ async def reset_password( @router.delete("/{user_id}", status_code=204) async def delete_user( user_id: int, - admin: Any = Depends(current_admin), + actor: Any = Depends(require_permission("users")), server: Any = Depends(get_server), ) -> None: - if user_id == admin.id: + if user_id == actor.id: raise OctopError(ErrorCode.FORBIDDEN, "cannot delete yourself") row = server.user_manager.get_row(user_id) if row is None: diff --git a/src/octop/api/routers/voice.py b/src/octop/api/routers/voice.py index cf4f1832..294e549f 100644 --- a/src/octop/api/routers/voice.py +++ b/src/octop/api/routers/voice.py @@ -9,7 +9,7 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field -from octop.api.deps import current_admin, current_user, get_server +from octop.api.deps import current_user, get_server, require_permission from octop.infra.errors import ErrorCode, OctopError from octop.infra.voice.manager import VoiceManager from octop.infra.voice.presets import load_voice_presets @@ -99,7 +99,7 @@ async def get_active_voice( @router.put("/active") async def set_active_voice( body: ActiveVoiceBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> dict[str, str]: return _voice_manager(server).set_active(stt=body.stt, tts=body.tts) @@ -148,7 +148,7 @@ async def _stream() -> AsyncIterator[bytes]: @admin_router.get("") async def admin_list_voice_providers( - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: return [_row_to_dict(r) for r in server.services.voice_provider_repo.list_all()] @@ -157,7 +157,7 @@ async def admin_list_voice_providers( @admin_router.post("", status_code=201) async def admin_create_voice_provider( body: VoiceProviderCreateBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> dict[str, Any]: repo = server.services.voice_provider_repo @@ -183,7 +183,7 @@ async def admin_create_voice_provider( async def admin_patch_voice_provider( provider_id: int, body: VoiceProviderPatchBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> dict[str, Any]: repo = server.services.voice_provider_repo @@ -208,7 +208,7 @@ async def admin_patch_voice_provider( @admin_router.delete("/{provider_id}", status_code=204) async def admin_delete_voice_provider( provider_id: int, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> None: repo = server.services.voice_provider_repo @@ -228,7 +228,7 @@ async def admin_delete_voice_provider( async def admin_test_voice_provider( provider_id: int, body: VoiceTestBody, - _: Any = Depends(current_admin), + _: Any = Depends(require_permission("voice")), server: Any = Depends(get_server), ) -> dict[str, Any]: return await _voice_manager(server).test_provider(provider_id, mode=body.mode) diff --git a/src/octop/api/routers/workspace.py b/src/octop/api/routers/workspace.py index 555d5136..5761669d 100644 --- a/src/octop/api/routers/workspace.py +++ b/src/octop/api/routers/workspace.py @@ -120,7 +120,9 @@ async def list_tree( server: Any = Depends(get_server), ) -> list[dict[str, Any]]: """Single-level directory listing under ``path`` (agent must be running).""" - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) io_path = _workspace_io_path(path, from_workspace=from_workspace) result = await ws.als(io_path) if result is None: @@ -171,7 +173,9 @@ async def write_file( server: Any = Depends(get_server), ) -> dict[str, Any]: """Overwrite ``path`` with ``body.content`` (text).""" - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) converter = get_doc_converter(path) if converter is not None: # Editable-document paths are always stored as the binary document @@ -218,7 +222,9 @@ async def mkdir_workspace_dir( """Create a directory (and parents) under the agent workspace.""" _ = from_workspace # API surface; mutations always use workspace-relative paths. rel = _assert_workspace_mutable(path) - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) try: await ws.amkdir(rel) except Exception as exc: @@ -246,7 +252,9 @@ async def delete_workspace_file( """Remove a file or directory tree from the agent workspace.""" _ = from_workspace rel = _assert_workspace_mutable(path) - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) try: await ws.adelete(rel) except Exception as exc: @@ -274,7 +282,9 @@ async def move_workspace_file( _ = from_workspace src = _assert_workspace_mutable(path) dest = _assert_workspace_mutable(body.destination) - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) try: await ws.amove(src, dest) except Exception as exc: @@ -294,7 +304,9 @@ async def upload_file( server: Any = Depends(get_server), ) -> dict[str, Any]: """Upload a binary file via multipart ``file=@...``.""" - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) target = path or f"/{file.filename or 'upload.bin'}" data = await file.read() try: @@ -447,7 +459,9 @@ async def glob_files( user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) root = _workspace_io_path(path, from_workspace=from_workspace) if pattern in ("**/*.md", "*.md") and root == ".": ls_result = await ws.als(".") @@ -480,7 +494,9 @@ async def grep_files( user: Any = Depends(current_user), server: Any = Depends(get_server), ) -> list[dict[str, Any]]: - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) result = await ws.agrep(pattern, _workspace_io_path(path, from_workspace=from_workspace)) if result is None: raise OctopError(ErrorCode.NOT_FOUND, "grep failed") @@ -503,7 +519,9 @@ async def export_workspace_archive( server: Any = Depends(get_server), ) -> StreamingResponse: """Pack workspace files into a zip archive.""" - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) data = await export_workspace_zip(ws) filename = f"workspace-{agent_id}.zip" return StreamingResponse( @@ -532,7 +550,9 @@ async def import_workspace_archive( if not raw: raise OctopError(ErrorCode.SLASH_BAD_ARGS, "empty archive") - ws = await require_running_workspace(agent_id, user=user, as_user=as_user, server=server) + ws = await require_running_workspace( + agent_id, user=user, as_user=as_user, server=server, owner_only=True + ) local_ws = resolve_agent_workspace_dir(server, agent_id) result = await import_workspace_zip( ws, diff --git a/src/octop/cli/commands/backup.py b/src/octop/cli/commands/backup.py index 55b9a4ae..8503b9f5 100644 --- a/src/octop/cli/commands/backup.py +++ b/src/octop/cli/commands/backup.py @@ -7,6 +7,7 @@ import click from octop.config import load_config +from octop.infra.backup.auto import create_and_store_auto_backup from octop.infra.backup.system_archive import create_system_backup, restore_system_backup from octop.infra.db.factory import open_database from octop.infra.db.migrate import run_migrations @@ -55,6 +56,53 @@ def create(output: Path | None, home: Path | None) -> None: click.echo(f"wrote {dest} ({len(data)} bytes)") +@backup.group("auto") +def auto() -> None: + """Automatic backup status and one-shot run (schedule requires ``octop run``).""" + + +@auto.command("status") +@click.option( + "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." +) +def auto_status(home: Path | None) -> None: + """Print automatic backup settings from config.json.""" + paths = _paths(home) + config = load_config(paths.config) + backup_cfg = config.backup + click.echo(f"auto_enabled: {backup_cfg.auto_enabled}") + click.echo(f"schedule: {backup_cfg.schedule}") + click.echo(f"retention_count: {backup_cfg.retention_count}") + click.echo("note: the scheduler only runs inside an active `octop run` process") + + +@auto.command("run") +@click.option( + "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." +) +def auto_run(home: Path | None) -> None: + """Create one automatic backup now and apply retention pruning.""" + paths = _paths(home) + config = load_config(paths.config) + db = open_database(config, paths) + run_migrations(db) + services = build_shared_services(db=db, paths=paths, config=config) + rows = services.agent_repo.list_all() + try: + entry, deleted = create_and_store_auto_backup( + paths=paths, + agent_rows=rows, + pool=db, + db_config=config.database, + retention_count=config.backup.retention_count, + ) + finally: + db.close() + click.echo(f"wrote {paths.backup_file(entry.name)} ({entry.size} bytes)") + if deleted: + click.echo(f"pruned: {', '.join(deleted)}") + + @backup.command("restore") @click.argument("archive", type=click.Path(exists=True, path_type=Path)) @click.option( diff --git a/src/octop/cli/support/offline_ops.py b/src/octop/cli/support/offline_ops.py index fbadaaa1..603190dd 100644 --- a/src/octop/cli/support/offline_ops.py +++ b/src/octop/cli/support/offline_ops.py @@ -14,6 +14,7 @@ resolve_username, ) from octop.infra.agents.experts.catalog import ExpertCatalog, default_library_root +from octop.infra.agents.providers.model_flags import is_local_runtime_provider from octop.infra.cron.task_type import normalize_cron_task_type, require_cron_prompt from octop.infra.cron.trigger import build_trigger from octop.infra.errors import ErrorCode, OctopError @@ -327,6 +328,15 @@ def delete_provider_offline(provider_id: int, *, home: Path | None = None) -> No row = svc.provider_repo.get(provider_id) if row is None: raise OctopError(ErrorCode.NOT_FOUND, "provider not found") + if is_local_runtime_provider( + row.name, + provider_api_key=row.api_key, + provider_base_url=row.base_url, + ): + raise OctopError( + ErrorCode.PROVIDER_LOCAL_PROTECTED, + "local runtime providers cannot be deleted", + ) refs = svc.provider_repo.find_referencing_agent_ids(svc.agent_repo, row.name) if refs: raise OctopError( diff --git a/src/octop/config.py b/src/octop/config.py index 17991e3c..54e00cf1 100644 --- a/src/octop/config.py +++ b/src/octop/config.py @@ -83,6 +83,15 @@ class TlsConfig: http_port: int = 80 +@dataclass(frozen=True) +class BackupConfig: + """Automatic system backup settings persisted in config.json.""" + + auto_enabled: bool = False + schedule: str = "cron:0 4 * * *" + retention_count: int = 7 + + @dataclass(frozen=True) class OctopConfig: bind_host: str = "127.0.0.1" @@ -100,6 +109,7 @@ class OctopConfig: # False when config.json omits ``database`` (use PathLayout.db unless env overrides). database_in_file: bool = False tls: TlsConfig = field(default_factory=TlsConfig) + backup: BackupConfig = field(default_factory=BackupConfig) def _defaults_for_file() -> dict[str, Any]: @@ -135,11 +145,32 @@ def _parse_tls_section(raw: object) -> TlsConfig: ) +def _parse_backup_section(raw: object) -> BackupConfig: + if raw is None: + return BackupConfig() + if not isinstance(raw, dict): + raise ValueError("config.backup must be an object") + defaults = BackupConfig() + schedule = str(raw.get("schedule", defaults.schedule)).strip() or defaults.schedule + try: + retention = int(raw.get("retention_count", defaults.retention_count)) + except (TypeError, ValueError) as exc: + raise ValueError("config.backup.retention_count must be an integer") from exc + if retention < 1: + raise ValueError("config.backup.retention_count must be >= 1") + return BackupConfig( + auto_enabled=bool(raw.get("auto_enabled", defaults.auto_enabled)), + schedule=schedule, + retention_count=retention, + ) + + def _coerce_int(name: str, value: str, default: int) -> int: try: return int(value) except ValueError: - logger.warning("env %s=%r is not int; using %s", name, value, default) + # Do not log the raw env value — names like OCTOP_*_PASSWORD can flow here. + logger.warning("env %s is not int; using %s", name, default) return default @@ -149,7 +180,9 @@ def _coerce_bool(name: str, value: str, default: bool) -> bool: return True if normalized in {"0", "false", "no", "off"}: return False - logger.warning("env %s=%r is not bool; using %s", name, value, default) + # Do not log the raw env value — OCTOP_REQUIRE_SETUP_PASSWORD is classified as + # sensitive by CodeQL (py/clear-text-logging-sensitive-data). + logger.warning("env %s is not bool; using %s", name, default) return default @@ -314,6 +347,34 @@ def load_config(path: Path) -> OctopConfig: "OCTOP_REQUIRE_SETUP_PASSWORD", v, bool(merged["require_setup_password"]) ) + backup = _parse_backup_section(raw.get("backup")) + if v := os.environ.get("OCTOP_BACKUP_AUTO_ENABLED"): + backup = BackupConfig( + auto_enabled=_coerce_bool("OCTOP_BACKUP_AUTO_ENABLED", v, backup.auto_enabled), + schedule=backup.schedule, + retention_count=backup.retention_count, + ) + if v := os.environ.get("OCTOP_BACKUP_SCHEDULE"): + schedule = v.strip() or backup.schedule + backup = BackupConfig( + auto_enabled=backup.auto_enabled, + schedule=schedule, + retention_count=backup.retention_count, + ) + if v := os.environ.get("OCTOP_BACKUP_RETENTION_COUNT"): + retention = _coerce_int("OCTOP_BACKUP_RETENTION_COUNT", v, backup.retention_count) + if retention < 1: + logger.warning( + "env OCTOP_BACKUP_RETENTION_COUNT is < 1; using %s", + backup.retention_count, + ) + retention = backup.retention_count + backup = BackupConfig( + auto_enabled=backup.auto_enabled, + schedule=backup.schedule, + retention_count=retention, + ) + return OctopConfig( bind_host=merged["bind_host"], port=int(merged["port"]), @@ -329,4 +390,5 @@ def load_config(path: Path) -> OctopConfig: database=parse_database_config(merged_db), database_in_file=database_in_file, tls=_parse_tls_section(raw.get("tls")), + backup=backup, ) diff --git a/src/octop/i18n/__init__.py b/src/octop/i18n/__init__.py index c3d627be..9ae56717 100644 --- a/src/octop/i18n/__init__.py +++ b/src/octop/i18n/__init__.py @@ -11,7 +11,7 @@ ) from octop.i18n.domains.errors import error_message from octop.i18n.domains.skills import all_skill_labels, skill_display_name -from octop.i18n.domains.tools import all_tool_labels, tool_display_name +from octop.i18n.domains.tools import all_tool_labels, hitl_tool_catalog, tool_display_name from octop.i18n.loader import all_keys_for_locale, flatten_keys, lookup, tr from octop.infra.utils.locale import ( DEFAULT_LOCALE, @@ -28,6 +28,7 @@ "all_keys_for_locale", "all_skill_labels", "all_tool_labels", + "hitl_tool_catalog", "channel_probe_field_label", "channel_probe_incomplete", "channel_runtime_reason", diff --git a/src/octop/i18n/domains/__init__.py b/src/octop/i18n/domains/__init__.py index 5a38daa2..9e84cd93 100644 --- a/src/octop/i18n/domains/__init__.py +++ b/src/octop/i18n/domains/__init__.py @@ -23,7 +23,13 @@ format_stream_error, stream_error_message, ) -from octop.i18n.domains.tools import all_tool_labels, tool_display_name +from octop.i18n.domains.tools import ( + HITL_TOOL_EXCLUDE, + HitlToolCatalogEntry, + all_tool_labels, + hitl_tool_catalog, + tool_display_name, +) __all__ = [ "MODEL_REF_UNAVAILABLE", @@ -38,7 +44,10 @@ "classify_stream_error_message", "format_agent_start_error", "format_stream_error", + "HITL_TOOL_EXCLUDE", + "HitlToolCatalogEntry", "all_tool_labels", + "hitl_tool_catalog", "channel_tool_hint_end", "channel_tool_hint_start", "error_message", diff --git a/src/octop/i18n/domains/tools.py b/src/octop/i18n/domains/tools.py index 8f46aebd..38f62610 100644 --- a/src/octop/i18n/domains/tools.py +++ b/src/octop/i18n/domains/tools.py @@ -2,12 +2,54 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any from octop.i18n.loader import _load_all, lookup from octop.infra.utils.locale import Locale, normalize_locale -__all__ = ["all_tool_labels", "tool_display_name"] +# Built-in tools that must stay available without HITL — hidden from the approval picker. +HITL_TOOL_EXCLUDE: frozenset[str] = frozenset( + { + # Meta / agent internals + "unknown", + "current_time", + "write_todos", + # Memory + "memory_search", + "memory_get", + # Knowledge base + "search_knowledge", + # Cron + "cronjob_list", + "cronjob_get", + "cronjob_create", + "cronjob_update", + "cronjob_delete", + "cronjob_run_now", + # Sub-agent / multi-agent orchestration + "task", + "agent_list", + "ask_agent", + "call_agent", + "acp_runner", + } +) + +__all__ = [ + "HITL_TOOL_EXCLUDE", + "HitlToolCatalogEntry", + "all_tool_labels", + "hitl_tool_catalog", + "tool_display_name", +] + + +@dataclass(frozen=True) +class HitlToolCatalogEntry: + name: str + label_zh: str + label_en: str def tool_display_name(name: str | None, locale: str | Locale = "en") -> str: @@ -26,3 +68,18 @@ def all_tool_labels(locale: str | Locale = "en") -> dict[str, str]: if not isinstance(node, dict): return {} return {str(k): str(v) for k, v in node.items() if isinstance(v, str)} + + +def hitl_tool_catalog() -> list[HitlToolCatalogEntry]: + """Built-in tools eligible for human-in-the-loop approval (excludes must-use internals).""" + labels_zh = all_tool_labels("zh") + labels_en = all_tool_labels("en") + names = sorted((set(labels_zh) | set(labels_en)) - HITL_TOOL_EXCLUDE) + return [ + HitlToolCatalogEntry( + name=name, + label_zh=labels_zh.get(name, name), + label_en=labels_en.get(name, name), + ) + for name in names + ] diff --git a/src/octop/i18n/en.json b/src/octop/i18n/en.json index 8e5970fa..55c73726 100644 --- a/src/octop/i18n/en.json +++ b/src/octop/i18n/en.json @@ -284,6 +284,7 @@ "SETUP_REQUIRED": "Initial setup is required.", "DATABASE_NOT_EMPTY": "The target database already has users. Use an empty database, or log in with the existing admin.", "BACKUP_DRIVER_MISMATCH": "This backup was made with a different database engine (SQLite vs PostgreSQL). Switch the runtime to match the backup, or create a new backup on the current engine. Cross-engine restore is not supported.", + "BACKUP_IN_PROGRESS": "A backup is already in progress. Try again shortly.", "FORBIDDEN": "Permission denied.", "NOT_FOUND": "Not found.", "USER_DISABLED": "This account has been disabled.", @@ -302,6 +303,7 @@ "PROVIDER_NAME_TAKEN": "Provider name is already in use.", "PROVIDER_NOT_VISIBLE": "Provider is not visible.", "PROVIDER_REFERENCED": "Provider is in use and cannot be removed.", + "PROVIDER_LOCAL_PROTECTED": "Local runtime providers cannot be deleted.", "STORAGE_BACKEND_NAME_TAKEN": "Storage backend name is already in use.", "STORAGE_BACKEND_REFERENCED": "Storage backend is in use and cannot be removed.", "CHANNEL_KIND_UNSUPPORTED": "Unsupported channel type.", @@ -332,13 +334,25 @@ "SKILL_PACKAGE_NOT_FOUND": "Skill package not found.", "SKILL_PACKAGE_NAME_TAKEN": "Skill package {name} already exists.", "SKILL_PACKAGE_BACKEND_UNSUPPORTED": "Skill packages currently support only “Full local environment (filesystem + shell)” or “Local filesystem (no shell commands)”, with storage root / (POSIX) or the agent workspace root (Windows default).", + "PUBLISHED_EXPERT_SLUG_TAKEN": "Published expert slug {slug} already exists.", + "PUBLISHED_EXPERT_ALREADY_EXISTS": "This agent is already published as “{name}”. Refresh the published template instead.", + "OIDC_BAD_REQUEST": "Invalid OIDC request: {detail}", "EXPERT_MARKET_FAILED": "Expert market is temporarily unavailable. Please try again later.", "SKILLHUB_SSL_FAILED": "SkillHub request failed: Python SSL error detected — on macOS try `brew reinstall openssl@3`, or check your system CA certificates.", "DESKTOP_SESSION_LIMIT": "Too many concurrent remote desktop streams (max {limit}). Close other connections and try again.", "DESKTOP_CAPTURE_FAILED": "Screen capture failed repeatedly. On Linux servers restart the virtual desktop; on macOS grant Screen Recording and Accessibility, then reconnect.", "PLUGIN_INVALID_ARCHIVE": "The download is not a valid plugin ZIP. Use a direct .zip download URL (on GitHub use raw.githubusercontent.com or the “Download” / “raw” link, not the /blob/ page).", "PLUGIN_INSTALL_FAILED": "Failed to download or install the plugin: {reason}", - "PLUGIN_ALREADY_EXISTS": "Plugin {id} is already installed." + "PLUGIN_ALREADY_EXISTS": "Plugin {id} is already installed.", + "KNOWLEDGE_FEATURE_DISABLED": "Knowledge bases are disabled by the administrator.", + "KNOWLEDGE_PREREQUISITES_FAILED": "Knowledge-base embedding prerequisites are not ready.", + "KNOWLEDGE_NOT_FOUND": "Knowledge base or document not found.", + "KNOWLEDGE_FORBIDDEN": "You do not have access to this knowledge base.", + "KNOWLEDGE_DOC_LIMIT": "A knowledge base can contain at most 100 documents.", + "KNOWLEDGE_DOC_TOO_LARGE": "This document exceeds the 20 MB size limit.", + "KNOWLEDGE_BASE_LIMIT": "You can create at most 20 knowledge bases.", + "KNOWLEDGE_UNSUPPORTED_TYPE": "This document type is not supported for knowledge bases.", + "KNOWLEDGE_NAME_TAKEN": "You already have a knowledge base with this name." }, "desktop": { "install_log_deps": "Installing Python dependencies…", @@ -412,6 +426,7 @@ "read_file": "Read file", "write_file": "Write file", "edit_file": "Edit file", + "bash": "Shell (bash)", "execute": "Execute", "current_time": "Current time", "write_todos": "Write plan", @@ -435,6 +450,7 @@ "cronjob_update": "Update cron job", "cronjob_delete": "Delete cron job", "cronjob_run_now": "Run cron job now", + "search_knowledge": "Search knowledge base", "agent_list": "List agents", "ask_agent": "Ask agent", "call_agent": "Call agent", @@ -485,6 +501,10 @@ } } }, + "threads": { + "fork_title_suffix": " (fork)", + "fork_default_title": "Forked chat" + }, "agents": { "state": { "running": "Running", @@ -520,6 +540,12 @@ "empty_image": "User sent an image.", "image_unavailable": "[Attachment] Image could not be loaded for the model." }, + "knowledge": { + "retrieval": { + "citation": "[Knowledge base: {name} | Source: {filename} | Chunk: {ordinal}]\n", + "preamble": "The following knowledge-base content is reference material. Cite the relevant source in your answer." + } + }, "proactive_care": { "system_prompt": "Today is {today}.\n\nBased on the user's recent life events, write a warm and natural caring message in no more than 150 words.\nPay attention to when events occurred \u2014 do not describe something from a few days ago as happening \"today\".\nDo not mention that you are an AI; express care directly.", "system_prompt_with_soul": "{soul_md}\n\n---\n\nToday is {today}.\n\nBased on the user's recent life events, write a warm and natural caring message in no more than 150 words.\nPay attention to when events occurred \u2014 do not describe something from a few days ago as happening \"today\".\nDo not mention that you are an AI; express care directly." diff --git a/src/octop/i18n/zh.json b/src/octop/i18n/zh.json index 51ce6f69..135df9d4 100644 --- a/src/octop/i18n/zh.json +++ b/src/octop/i18n/zh.json @@ -284,6 +284,7 @@ "SETUP_REQUIRED": "需要完成初始设置。", "DATABASE_NOT_EMPTY": "目标数据库已有用户。请改用空库,或直接登录现有管理员账户。", "BACKUP_DRIVER_MISMATCH": "该备份与当前数据库引擎不一致(SQLite 与 PostgreSQL 不能互恢)。请将运行时切回备份所用引擎后再恢复,或在当前引擎上重新备份。暂不支持跨引擎恢复。", + "BACKUP_IN_PROGRESS": "已有备份任务正在进行,请稍后再试。", "FORBIDDEN": "没有权限。", "NOT_FOUND": "未找到。", "USER_DISABLED": "账号已禁用。", @@ -302,6 +303,7 @@ "PROVIDER_NAME_TAKEN": "提供商名称已被占用。", "PROVIDER_NOT_VISIBLE": "提供商不可见。", "PROVIDER_REFERENCED": "提供商正在使用中,无法删除。", + "PROVIDER_LOCAL_PROTECTED": "本地提供商无法删除。", "STORAGE_BACKEND_NAME_TAKEN": "存储后端名称已被占用。", "STORAGE_BACKEND_REFERENCED": "存储后端正在使用中,无法删除。", "CHANNEL_KIND_UNSUPPORTED": "不支持的通道类型。", @@ -332,13 +334,25 @@ "SKILL_PACKAGE_NOT_FOUND": "未找到技能包。", "SKILL_PACKAGE_NAME_TAKEN": "技能包 {name} 已存在。", "SKILL_PACKAGE_BACKEND_UNSUPPORTED": "技能包目前仅支持「本地完整环境(文件系统+指令执行)」或「本地文件系统(不能执行指令)」,且存储根目录须为 /(POSIX)或 Agent 工作区根目录(Windows 默认)。", + "PUBLISHED_EXPERT_SLUG_TAKEN": "已存在发布专家 slug:{slug}。", + "PUBLISHED_EXPERT_ALREADY_EXISTS": "该专家已发布为「{name}」,请改为刷新已发布模板。", + "OIDC_BAD_REQUEST": "OIDC 请求无效:{detail}", "EXPERT_MARKET_FAILED": "专家市场暂时不可用,请稍后重试。", "SKILLHUB_SSL_FAILED": "SkillHub 请求失败:检测到 Python SSL 错误。macOS 可尝试 `brew reinstall openssl@3`,或检查系统 CA 证书。", "DESKTOP_SESSION_LIMIT": "远程桌面并发连接已达上限(最多 {limit} 路),请关闭其他连接后重试。", "DESKTOP_CAPTURE_FAILED": "连续抓屏失败。Linux 服务器请重启虚拟桌面;macOS 请在系统设置中授予屏幕录制与辅助功能权限后重连。", "PLUGIN_INVALID_ARCHIVE": "下载内容不是有效的插件 ZIP。请使用直接的 .zip 下载地址(GitHub 请用 raw.githubusercontent.com 或 “Download/raw” 链接,不要用 /blob/ 页面地址)。", "PLUGIN_INSTALL_FAILED": "下载或安装插件失败:{reason}", - "PLUGIN_ALREADY_EXISTS": "插件 {id} 已安装。" + "PLUGIN_ALREADY_EXISTS": "插件 {id} 已安装。", + "KNOWLEDGE_FEATURE_DISABLED": "管理员尚未启用知识库功能。", + "KNOWLEDGE_PREREQUISITES_FAILED": "知识库的向量模型或依赖尚未就绪。", + "KNOWLEDGE_NOT_FOUND": "未找到知识库或文档。", + "KNOWLEDGE_FORBIDDEN": "您没有此知识库的访问权限。", + "KNOWLEDGE_DOC_LIMIT": "每个知识库最多可包含 100 个文档。", + "KNOWLEDGE_DOC_TOO_LARGE": "文档超过 20 MB 大小限制。", + "KNOWLEDGE_BASE_LIMIT": "每个用户最多可创建 20 个知识库。", + "KNOWLEDGE_UNSUPPORTED_TYPE": "知识库不支持此文档类型。", + "KNOWLEDGE_NAME_TAKEN": "您已有同名知识库。" }, "desktop": { "install_log_deps": "正在安装 Python 依赖…", @@ -412,7 +426,8 @@ "read_file": "读取文件", "write_file": "写入文件", "edit_file": "编辑文件", - "execute": "执行", + "bash": "Shell 命令 (bash)", + "execute": "执行指令", "current_time": "当前时间", "write_todos": "编写计划", "task": "子智能体任务", @@ -435,6 +450,7 @@ "cronjob_update": "更新定时任务", "cronjob_delete": "删除定时任务", "cronjob_run_now": "立即执行定时任务", + "search_knowledge": "检索知识库", "agent_list": "列出 Agent", "ask_agent": "咨询 Agent", "call_agent": "调用 Agent", @@ -485,6 +501,10 @@ } } }, + "threads": { + "fork_title_suffix": "(分叉)", + "fork_default_title": "分叉对话" + }, "agents": { "state": { "running": "运行中", @@ -520,6 +540,12 @@ "empty_image": "用户发送了图片。", "image_unavailable": "[附件] 图片无法加载到模型。" }, + "knowledge": { + "retrieval": { + "citation": "[知识库:{name}|来源:{filename}|片段:{ordinal}]\n", + "preamble": "以下知识库内容仅供参考;回答时请引用对应来源。" + } + }, "proactive_care": { "system_prompt": "今天是 {today}。\n\n请根据用户近期的生活事件,用温暖自然的语气写一段关心的话,不超过 150 字。\n注意事件发生的时间,不要把几天前的事说成「今天」。\n不要提及你是 AI,直接表达关心即可。", "system_prompt_with_soul": "{soul_md}\n\n---\n\n今天是 {today}。\n\n请根据用户近期的生活事件,用温暖自然的语气写一段关心的话,不超过 150 字。\n注意事件发生的时间,不要把几天前的事说成「今天」。\n不要提及你是 AI,直接表达关心即可。" diff --git a/src/octop/infra/agents/builtin_skills/__init__.py b/src/octop/infra/agents/builtin_skills/__init__.py new file mode 100644 index 00000000..69089e27 --- /dev/null +++ b/src/octop/infra/agents/builtin_skills/__init__.py @@ -0,0 +1,60 @@ +"""Octop-owned built-in Skills seeded into every agent workspace.""" + +from __future__ import annotations + +from importlib import resources +from importlib.resources.abc import Traversable +from typing import Any + +OCTOP_BUILTIN_SKILLS_ROOT = "_builtin_skills" +RETIRED_BUILTIN_SKILLS = ("install-skill",) +_WORKSPACE_TOKEN = b"{{OCTOP_WORKSPACE}}" +_PACKAGE = "octop.infra.agents.builtin_skills" + + +def _collect_files(source: Traversable, prefix: str, out: list[tuple[str, bytes]]) -> None: + for entry in source.iterdir(): + if entry.name.startswith((".", "__")): + continue + path = f"{prefix}/{entry.name}" + if entry.is_dir(): + _collect_files(entry, path, out) + else: + out.append((path, entry.read_bytes())) + + +async def sync_octop_builtin_skills(workspace: Any) -> list[str]: + """Overwrite Octop-owned built-ins and remove superseded runtime copies.""" + # DeepAgents scans both roots on the first turn. Keep the writable root + # present even before the user installs their first Skill so that scan is + # clean on fresh expert instances. + await workspace.amkdir("skills") + + package_root = resources.files(_PACKAGE) + uploads: list[tuple[str, bytes]] = [] + skill_names: list[str] = [] + for entry in package_root.iterdir(): + if entry.name.startswith((".", "__")) or not entry.is_dir(): + continue + if not entry.joinpath("SKILL.md").is_file(): + continue + skill_names.append(entry.name) + _collect_files(entry, f"{OCTOP_BUILTIN_SKILLS_ROOT}/{entry.name}", uploads) + + # Render the exact backend-visible workspace path into the instructions. + # This prevents an agent from confusing Octop's per-expert workspace with + # a legacy ~/.harness-agent/workspace directory discovered through HOME. + workspace_path = workspace.resolve_path(".").encode("utf-8") + uploads = [(path, data.replace(_WORKSPACE_TOKEN, workspace_path)) for path, data in uploads] + + if uploads: + await workspace.aupload_many(uploads) + + for name in RETIRED_BUILTIN_SKILLS: + path = f"{OCTOP_BUILTIN_SKILLS_ROOT}/{name}" + if await workspace.aexists(path): + await workspace.adelete(path) + return sorted(skill_names) + + +__all__ = ["OCTOP_BUILTIN_SKILLS_ROOT", "sync_octop_builtin_skills"] diff --git a/src/octop/infra/agents/builtin_skills/skill-manager/SKILL.md b/src/octop/infra/agents/builtin_skills/skill-manager/SKILL.md new file mode 100644 index 00000000..fc175f49 --- /dev/null +++ b/src/octop/infra/agents/builtin_skills/skill-manager/SKILL.md @@ -0,0 +1,62 @@ +--- +name: skill-manager +description: 必须用于通过对话管理当前专家实例的 Skill,包括查找、检查、安装、导入、更新、编辑、列出、恢复或删除。支持 skillhub.cn/skills/... 页面 URL、SkillHub CLI、本地及上传文件、SKILL.md、ZIP/TAR、Git/GitHub 和普通下载 URL;也用于把 PDF、Markdown、文档、代码、图片等任意资料整理成新 Skill。Must use for conversational Skill management from files, URLs, Git repositories, archives, or SkillHub. +--- + +# Skill Manager + +只管理当前实例的 `{{OCTOP_WORKSPACE}}/skills/`。当前实例工作区固定为 `{{OCTOP_WORKSPACE}}`;上传文件通常位于其中的 `inbound/`。不要用 `pwd`、`$HOME`、`~/.harness-agent/workspace`、记忆文件或搜索结果重新猜测工作区。不要改其他实例、全局 Skill 目录或 `_builtin_skills/`。 + +## 操作入口 + +使用内置脚本完成检查、下载、安全解包、安装、列出和删除: + +```bash +python "{{OCTOP_WORKSPACE}}/_builtin_skills/skill-manager/scripts/manage_skills.py" +``` + +```bash +# 列出已安装技能 +... list + +# 检查来源,不写入 skills/ +... inspect "" + +# 安装;仓库或压缩包内的多个 Skill 可一次安装 +... install "" [--subpath "path/in/repo"] [--name "slug"] + +# 仅在用户明确同意替换后覆盖 +... install "" --force + +# SkillHub 搜索;结果可用 skillhub: 检查或安装 +... skillhub-search "" --limit 10 + +# 仅在用户明确同意删除后执行;实际移动到 skills/.trash/ +... remove "" --yes + +# 从回收站恢复 +... restore "" +``` + +## 来源处理 + +1. **现成 Skill、目录或 ZIP/TAR**:先 `inspect`;检查通过且用户已要求安装时执行 `install`。 +2. **Git/GitHub URL**:可直接处理仓库、GitHub `tree` 子目录和指向 `SKILL.md` 的 `blob` URL。复杂仓库使用 `git+` 和 `--subpath`。 +3. **SkillHub 页面 URL**:`https://skillhub.cn/skills//` 可直接传给 `inspect` 或 `install`;脚本会解析 namespace 和 slug。不要自行安装/升级 CLI,不要直接运行 `skillhub install`。 +4. **普通 HTTP(S) URL**:脚本处理直接文件和压缩包。若 URL 是其他介绍网页,先用现有网页工具读取并找到公开仓库、下载地址或 `SKILL.md`,再交给脚本。不得绕过登录、付费墙或访问控制。 +5. **SkillHub 搜索**:用户只描述能力时,先搜索并给出 1–3 个候选的 slug、名称、用途和来源,让用户选择;用户已点名具体 Skill 时,可直接检查并安装。 +6. **任意非 Skill 文件**:普通资料不会因复制而自动成为 Skill。先读取和理解材料,再使用内置 `skill-creator` 工作流,将可复用知识、步骤和必要资源整理为合法 Skill,写入 `{{OCTOP_WORKSPACE}}/skills//`,然后执行 `inspect` 校验。 + +## 变更规则 + +- 新安装请求本身即表示同意新增;目标已存在时停止并说明冲突,只有获得明确同意才使用 `--force`。 +- 编辑现有 Skill 前先读取其 `SKILL.md` 和相关资源,只改该 Skill 目录;修改后重新 `inspect`。 +- 删除前点名目标并取得明确确认。使用 `remove --yes`,不要直接 `rm -rf`。需要恢复时使用 `restore`。 +- 安装后报告 slug 和最终路径。新安装、更新或删除的 Skill 通常从下一次新会话开始完整生效。 + +## 安全边界 + +- 脚本限制下载及解包体积、文件数,拒绝路径穿越和符号链接,并通过暂存目录安装。 +- 不执行、导入或 source 下载包中的代码。安装表示部署 Skill 内容,不代表第三方代码已通过安全审计。 +- 不在输出中回显 URL 凭据;私有来源只使用环境中已有的 Git 或 SkillHub 凭据。 +- 若当前实例没有 Shell 执行能力,仍可用文件工具处理简单文本 Skill,但必须说明 URL 下载、安全解包和 SkillHub CLI 无法完成,不能假装成功。 diff --git a/src/octop/infra/agents/builtin_skills/skill-manager/scripts/manage_skills.py b/src/octop/infra/agents/builtin_skills/skill-manager/scripts/manage_skills.py new file mode 100644 index 00000000..dbfaf524 --- /dev/null +++ b/src/octop/infra/agents/builtin_skills/skill-manager/scripts/manage_skills.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +"""Safely inspect, install, list, remove, and restore workspace Skills.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +import zipfile +from pathlib import Path, PurePosixPath +from typing import Any +from urllib.parse import unquote, urlparse + +import yaml + +MAX_FILES = 2_000 +MAX_BYTES = 64 * 1024 * 1024 +MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024 +RESERVED_SKILL_SLUGS = frozenset({"skill-manager"}) +_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") +_NAMESPACE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$") + + +class SkillManagerError(RuntimeError): + """A safe, user-facing Skill management failure.""" + + +def _emit(payload: Any) -> None: + print(json.dumps(payload, ensure_ascii=False, indent=2)) + + +def _redact_urls(value: str) -> str: + return re.sub(r"(?i)\b(?:https?|ssh|git)://[^\s'\"]+", "", value) + + +def _slug(value: str) -> str: + normalized = re.sub(r"[^a-z0-9_-]+", "-", value.strip().lower()).strip("-_") + if not _SLUG_RE.fullmatch(normalized): + raise SkillManagerError(f"invalid skill name: {value!r}") + return normalized + + +def _workspace(value: str) -> Path: + path = Path(value).expanduser() + if not path.is_absolute(): + raise SkillManagerError("--workspace must be an absolute path") + path = path.resolve() + if path == Path(path.anchor): + raise SkillManagerError("refusing to use a filesystem root as the workspace") + if not path.is_dir(): + raise SkillManagerError(f"workspace does not exist: {path}") + return path + + +def _default_workspace() -> Path: + """Infer the workspace only from this deployed built-in's location.""" + script = Path(__file__).resolve() + try: + workspace = script.parents[3] + except IndexError as exc: # pragma: no cover - installed layout always has these parents + raise SkillManagerError("cannot infer workspace from the manager script path") from exc + if script.parents[1].name != "skill-manager" or script.parents[2].name != "_builtin_skills": + raise SkillManagerError("--workspace is required outside a deployed expert workspace") + return _workspace(str(workspace)) + + +def _skillhub_source(source: str) -> tuple[str, str] | None: + """Return ``(slug, namespace)`` for SkillHub shorthand or page URLs.""" + if source.startswith("skillhub:"): + value = source.partition(":")[2].strip().strip("/") + parts = value.split("/") + if len(parts) == 1: + return _slug(parts[0]), "" + if len(parts) == 2: + namespace, slug = parts + if not _NAMESPACE_RE.fullmatch(namespace): + raise SkillManagerError(f"invalid SkillHub namespace: {namespace!r}") + return _slug(slug), namespace + raise SkillManagerError( + "SkillHub source must be skillhub: or skillhub:/" + ) + + parsed = urlparse(source) + if parsed.hostname not in {"skillhub.cn", "www.skillhub.cn"}: + return None + parts = [unquote(part) for part in parsed.path.strip("/").split("/") if part] + if len(parts) != 3 or parts[0] != "skills": + raise SkillManagerError( + "unsupported SkillHub URL; expected https://skillhub.cn/skills//" + ) + namespace, slug = parts[1:] + if not _NAMESPACE_RE.fullmatch(namespace): + raise SkillManagerError(f"invalid SkillHub namespace: {namespace!r}") + return _slug(slug), namespace + + +def _safe_relative(name: str) -> Path: + if not name or "\\" in name or "\x00" in name: + raise SkillManagerError(f"unsafe archive path: {name!r}") + path = PurePosixPath(name) + if path.is_absolute() or ".." in path.parts: + raise SkillManagerError(f"unsafe archive path: {name!r}") + parts = [part for part in path.parts if part not in {"", "."}] + if not parts: + raise SkillManagerError(f"invalid archive path: {name!r}") + return Path(*parts) + + +def _safe_target(root: Path, name: str) -> Path: + target = (root / _safe_relative(name)).resolve() + try: + target.relative_to(root.resolve()) + except ValueError as exc: + raise SkillManagerError(f"archive path escapes destination: {name!r}") from exc + return target + + +def _extract_zip(source: Path, target: Path) -> None: + with zipfile.ZipFile(source) as archive: + infos = archive.infolist() + files = [info for info in infos if not info.is_dir()] + if len(files) > MAX_FILES: + raise SkillManagerError("archive contains too many files") + if sum(info.file_size for info in files) > MAX_BYTES: + raise SkillManagerError("archive expands beyond 64 MB") + for info in infos: + if stat.S_ISLNK(info.external_attr >> 16): + raise SkillManagerError(f"archive contains a symlink: {info.filename}") + destination = _safe_target(target, info.filename) + if info.is_dir(): + destination.mkdir(parents=True, exist_ok=True) + continue + if info.compress_size and info.file_size / info.compress_size > 1_000: + raise SkillManagerError(f"suspicious compression ratio: {info.filename}") + destination.parent.mkdir(parents=True, exist_ok=True) + with archive.open(info) as src, destination.open("wb") as dst: + shutil.copyfileobj(src, dst) + + +def _extract_tar(source: Path, target: Path) -> None: + with tarfile.open(source) as archive: + members = archive.getmembers() + files = [member for member in members if member.isfile()] + if len(files) > MAX_FILES: + raise SkillManagerError("archive contains too many files") + if sum(member.size for member in files) > MAX_BYTES: + raise SkillManagerError("archive expands beyond 64 MB") + for member in members: + destination = _safe_target(target, member.name) + if member.isdir(): + destination.mkdir(parents=True, exist_ok=True) + continue + if not member.isfile(): + raise SkillManagerError(f"unsupported archive entry: {member.name}") + stream = archive.extractfile(member) + if stream is None: + raise SkillManagerError(f"cannot read archive entry: {member.name}") + destination.parent.mkdir(parents=True, exist_ok=True) + with stream, destination.open("wb") as dst: + shutil.copyfileobj(stream, dst) + + +def _download(url: str, target: Path) -> None: + request = urllib.request.Request(url, headers={"User-Agent": "octop-skill-manager/1.0"}) + total = 0 + try: + with urllib.request.urlopen(request, timeout=30) as response, target.open("wb") as output: + while chunk := response.read(1024 * 1024): + total += len(chunk) + if total > MAX_DOWNLOAD_BYTES: + raise SkillManagerError("download exceeds 256 MB") + output.write(chunk) + except SkillManagerError: + raise + except OSError as exc: + host = urlparse(url).hostname or "remote host" + raise SkillManagerError(f"download failed from {host}") from exc + + +def _run(command: list[str], *, timeout: int = 120) -> subprocess.CompletedProcess[str]: + try: + result = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except FileNotFoundError as exc: + raise SkillManagerError(f"required command is not installed: {command[0]}") from exc + except subprocess.TimeoutExpired as exc: + raise SkillManagerError(f"command timed out: {command[0]}") from exc + if result.returncode: + detail = result.stderr.strip() or result.stdout.strip() or f"exit {result.returncode}" + raise SkillManagerError(_redact_urls(detail)) + return result + + +def _command(name: str, *args: str) -> list[str]: + """Resolve a CLI entry point, including Windows ``.cmd`` shims.""" + executable = shutil.which(name) + if executable is None: + raise SkillManagerError(f"required command is not installed: {name}") + return [executable, *args] + + +def _github_source(source: str) -> tuple[str, str, str] | None: + parsed = urlparse(source) + if parsed.hostname not in {"github.com", "www.github.com"}: + return None + parts = [unquote(part) for part in parsed.path.strip("/").split("/") if part] + if len(parts) < 2: + return None + owner, repo = parts[0], parts[1].removesuffix(".git") + ref = "" + subpath = "" + if len(parts) >= 4 and parts[2] in {"tree", "blob"}: + ref = parts[3] + subpath = "/".join(parts[4:]) + if parts[2] == "blob" and subpath.endswith("/SKILL.md"): + subpath = subpath[: -len("/SKILL.md")] + elif parts[2] == "blob" and subpath == "SKILL.md": + subpath = "" + return f"https://github.com/{owner}/{repo}.git", ref, subpath + + +def _clone(source: str, target: Path, *, ref: str = "") -> None: + command = ["git", "clone", "--depth", "1"] + if ref: + command.extend(["--branch", ref]) + command.extend([source, str(target)]) + _run(command) + + +def _manifest(text: str) -> dict[str, Any]: + match = re.match(r"^---\s*\n(.*?)\n---(?:\s*\n|$)", text, re.DOTALL) + if match is None: + raise SkillManagerError("SKILL.md is missing YAML frontmatter") + try: + metadata = yaml.safe_load(match.group(1)) or {} + except yaml.YAMLError as exc: + raise SkillManagerError(f"SKILL.md frontmatter is invalid: {exc}") from exc + if not isinstance(metadata, dict): + raise SkillManagerError("SKILL.md frontmatter must be a mapping") + for field in ("name", "description"): + if not str(metadata.get(field) or "").strip(): + raise SkillManagerError(f"SKILL.md frontmatter is missing {field}") + return metadata + + +def _materialize_file(source: Path, target: Path) -> Path: + if zipfile.is_zipfile(source): + unpacked = target / "unpacked" + unpacked.mkdir() + _extract_zip(source, unpacked) + return unpacked + if tarfile.is_tarfile(source): + unpacked = target / "unpacked" + unpacked.mkdir() + _extract_tar(source, unpacked) + return unpacked + try: + text = source.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as exc: + raise SkillManagerError( + "input is not a Skill directory, SKILL.md, ZIP, TAR, or readable text" + ) from exc + _manifest(text) + single = target / "single" + single.mkdir() + shutil.copy2(source, single / "SKILL.md") + return single + + +def _select_subpath(root: Path, subpath: str) -> Path: + if not subpath: + return root + selected = (root / _safe_relative(subpath)).resolve() + try: + selected.relative_to(root.resolve()) + except ValueError as exc: + raise SkillManagerError(f"subpath escapes source: {subpath!r}") from exc + if not selected.exists(): + raise SkillManagerError(f"subpath does not exist: {subpath}") + return selected.parent if selected.is_file() and selected.name == "SKILL.md" else selected + + +def _materialize(source: str, target: Path) -> Path: + skillhub_source = _skillhub_source(source) + if skillhub_source is not None: + slug, namespace = skillhub_source + installed = target / "skillhub" + installed.mkdir() + command = _command("skillhub", "--skip-self-upgrade", "install", slug) + if namespace: + command.extend(["--namespace", namespace]) + command.extend(["--dir", str(installed), "--json"]) + _run(command) + return installed + + local = Path(source).expanduser() + if local.exists(): + resolved = local.resolve() + return resolved if resolved.is_dir() else _materialize_file(resolved, target) + + parsed = urlparse(source.removeprefix("git+")) + if parsed.scheme not in {"http", "https", "ssh", "git"}: + raise SkillManagerError("source does not exist and is not a supported URL") + if parsed.scheme in {"http", "https"} and (parsed.username or parsed.password): + raise SkillManagerError("credentials embedded in URLs are not supported") + + github = _github_source(source) + if github is not None: + repo_url, ref, subpath = github + checkout = target / "repo" + _clone(repo_url, checkout, ref=ref) + return _select_subpath(checkout, subpath) + + if source.startswith("git+") or source.endswith(".git"): + checkout = target / "repo" + _clone(source.removeprefix("git+"), checkout) + return checkout + + downloaded = target / "download" + _download(source, downloaded) + return _materialize_file(downloaded, target) + + +def _skill_roots(root: Path) -> list[Path]: + direct = root / "SKILL.md" + manifests = [direct] if direct.is_file() else sorted(root.rglob("SKILL.md")) + selected: list[Path] = [] + for manifest in manifests: + relative = manifest.relative_to(root) + if any(part.startswith(".") or part == "__MACOSX" for part in relative.parts): + continue + candidate = manifest.parent.resolve() + if any(candidate == parent or candidate.is_relative_to(parent) for parent in selected): + continue + selected.append(candidate) + if not selected: + raise SkillManagerError("no SKILL.md was found in the source") + return selected + + +def _validate_tree(root: Path) -> dict[str, Any]: + if root.is_symlink() or not root.is_dir(): + raise SkillManagerError(f"skill root is not a regular directory: {root}") + manifest_path = root / "SKILL.md" + try: + metadata = _manifest(manifest_path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError) as exc: + raise SkillManagerError(f"cannot read {manifest_path}") from exc + count = 0 + total = 0 + for current, dirnames, filenames in os.walk(root, followlinks=False): + current_path = Path(current) + for dirname in list(dirnames): + child = current_path / dirname + if child.is_symlink(): + raise SkillManagerError(f"skill contains a symlink: {child}") + if dirname == ".git": + dirnames.remove(dirname) + for filename in filenames: + child = current_path / filename + if child.is_symlink() or not child.is_file(): + raise SkillManagerError(f"skill contains an unsupported file: {child}") + count += 1 + total += child.stat().st_size + if count > MAX_FILES: + raise SkillManagerError("skill contains too many files") + if total > MAX_BYTES: + raise SkillManagerError("skill exceeds 64 MB") + return { + "name": str(metadata["name"]), + "description": str(metadata["description"]), + "files": count, + "bytes": total, + } + + +def _discover(source_root: Path, *, subpath: str = "", name: str = "") -> list[dict[str, Any]]: + roots = _skill_roots(_select_subpath(source_root, subpath)) + if name and len(roots) != 1: + raise SkillManagerError("--name can only be used when the source contains one skill") + found: list[dict[str, Any]] = [] + slugs: set[str] = set() + for root in roots: + summary = _validate_tree(root) + slug = _slug(name or str(summary["name"]) or root.name) + if slug in slugs: + raise SkillManagerError(f"duplicate skill name in source: {slug}") + slugs.add(slug) + found.append({"slug": slug, "root": root, **summary}) + return found + + +def _copy_skill(source: Path, target: Path) -> None: + def ignore(_directory: str, names: list[str]) -> set[str]: + return {name for name in names if name == ".git"} + + shutil.copytree(source, target, symlinks=False, ignore=ignore) + _validate_tree(target) + + +def _install(workspace: Path, skills: list[dict[str, Any]], *, force: bool) -> list[dict[str, Any]]: + skills_dir = workspace / "skills" + skills_dir.mkdir(parents=True, exist_ok=True) + reserved = [item["slug"] for item in skills if item["slug"] in RESERVED_SKILL_SLUGS] + if reserved: + raise SkillManagerError( + "cannot replace an Octop-owned built-in skill: " + ", ".join(reserved) + ) + conflicts = [item["slug"] for item in skills if (skills_dir / item["slug"]).exists()] + if conflicts and not force: + raise SkillManagerError( + "already installed; ask before using --force: " + ", ".join(conflicts) + ) + + staging = Path(tempfile.mkdtemp(prefix=".skill-manager-stage-", dir=skills_dir)) + backup = Path(tempfile.mkdtemp(prefix=".skill-manager-backup-", dir=skills_dir)) + installed: list[Path] = [] + moved_backups: list[tuple[Path, Path]] = [] + try: + for item in skills: + _copy_skill(item["root"], staging / item["slug"]) + for item in skills: + slug = item["slug"] + destination = skills_dir / slug + if destination.exists(): + saved = backup / slug + os.replace(destination, saved) + moved_backups.append((destination, saved)) + os.replace(staging / slug, destination) + installed.append(destination) + except Exception: + for destination in reversed(installed): + if destination.exists(): + shutil.rmtree(destination) + for destination, saved in reversed(moved_backups): + if saved.exists(): + os.replace(saved, destination) + raise + finally: + shutil.rmtree(staging, ignore_errors=True) + shutil.rmtree(backup, ignore_errors=True) + + return [ + { + "slug": item["slug"], + "path": str(skills_dir / item["slug"]), + "files": item["files"], + "bytes": item["bytes"], + } + for item in skills + ] + + +def _list(workspace: Path) -> list[dict[str, Any]]: + skills_dir = workspace / "skills" + if not skills_dir.is_dir(): + return [] + rows: list[dict[str, Any]] = [] + for entry in sorted(skills_dir.iterdir()): + if not entry.is_dir() or entry.name.startswith("."): + continue + try: + summary = _validate_tree(entry) + except SkillManagerError as exc: + rows.append({"slug": entry.name, "valid": False, "error": str(exc)}) + continue + rows.append({"slug": entry.name, "valid": True, **summary}) + return rows + + +def _remove(workspace: Path, name: str, *, confirmed: bool) -> dict[str, str]: + if not confirmed: + raise SkillManagerError("removal requires --yes after explicit user confirmation") + slug = _slug(name) + skills_dir = workspace / "skills" + source = skills_dir / slug + if source.is_symlink(): + raise SkillManagerError(f"refusing to remove a symlinked skill: {slug}") + if not source.is_dir(): + raise SkillManagerError(f"skill is not installed: {slug}") + trash = skills_dir / ".trash" + trash.mkdir(parents=True, exist_ok=True) + destination = trash / slug + suffix = 1 + while destination.exists(): + destination = trash / f"{slug}-{suffix}" + suffix += 1 + os.replace(source, destination) + (destination / ".skill-manager-trash.json").write_text( + json.dumps({"slug": slug}), + encoding="utf-8", + ) + return {"removed": slug, "trash_name": destination.name, "trash": str(destination)} + + +def _restore(workspace: Path, trash_name: str) -> dict[str, str]: + safe_name = _slug(trash_name) + skills_dir = workspace / "skills" + source = skills_dir / ".trash" / safe_name + if source.is_symlink(): + raise SkillManagerError(f"refusing to restore a symlinked trash entry: {safe_name}") + if not source.is_dir(): + raise SkillManagerError(f"trash entry does not exist: {safe_name}") + _validate_tree(source) + metadata_path = source / ".skill-manager-trash.json" + slug = safe_name + if metadata_path.is_file(): + try: + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + slug = _slug(str(metadata.get("slug") or safe_name)) + except (OSError, json.JSONDecodeError, SkillManagerError) as exc: + raise SkillManagerError(f"trash metadata is invalid: {safe_name}") from exc + destination = skills_dir / slug + if destination.exists(): + raise SkillManagerError(f"cannot restore because skill is installed: {slug}") + if metadata_path.exists(): + metadata_path.unlink() + os.replace(source, destination) + return {"restored": slug, "path": str(destination)} + + +def _effective_name(source: str, name: str) -> str: + if name: + return name + skillhub_source = _skillhub_source(source) + return skillhub_source[0] if skillhub_source is not None else "" + + +def _inspect(source: str, *, subpath: str, name: str) -> list[dict[str, Any]]: + with tempfile.TemporaryDirectory(prefix="skill-manager-source-") as tmp: + root = _materialize(source, Path(tmp)) + return [ + {key: value for key, value in item.items() if key != "root"} + for item in _discover( + root, + subpath=subpath, + name=_effective_name(source, name), + ) + ] + + +def _install_source( + workspace: Path, + source: str, + *, + subpath: str, + name: str, + force: bool, +) -> list[dict[str, Any]]: + with tempfile.TemporaryDirectory(prefix="skill-manager-source-") as tmp: + root = _materialize(source, Path(tmp)) + found = _discover( + root, + subpath=subpath, + name=_effective_name(source, name), + ) + return _install(workspace, found, force=force) + + +def _skillhub_search(query: str, limit: int) -> None: + result = _run( + _command( + "skillhub", + "--skip-self-upgrade", + "search", + "--json", + "--search-limit", + str(max(1, min(limit, 100))), + query, + ), + timeout=30, + ) + try: + _emit(json.loads(result.stdout)) + except json.JSONDecodeError as exc: + raise SkillManagerError("SkillHub returned invalid JSON") from exc + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--workspace", + default="", + help="absolute current agent workspace (normally inferred from the deployed script path)", + ) + commands = parser.add_subparsers(dest="command", required=True) + + commands.add_parser("list", help="list installed workspace skills") + + for command in ("inspect", "install"): + item = commands.add_parser( + command, + help=f"{command} a file, directory, URL, or skillhub:slug", + ) + item.add_argument("source") + item.add_argument("--subpath", default="") + item.add_argument("--name", default="") + if command == "install": + item.add_argument("--force", action="store_true") + + remove = commands.add_parser("remove", help="move an installed skill to skills/.trash") + remove.add_argument("name") + remove.add_argument("--yes", action="store_true") + + restore = commands.add_parser("restore", help="restore an entry from skills/.trash") + restore.add_argument("trash_name") + + search = commands.add_parser("skillhub-search", help="search with the SkillHub CLI") + search.add_argument("query") + search.add_argument("--limit", type=int, default=10) + return parser + + +def main() -> int: + args = _parser().parse_args() + try: + workspace = _workspace(args.workspace) if args.workspace else _default_workspace() + if args.command == "list": + _emit(_list(workspace)) + elif args.command == "inspect": + _emit(_inspect(args.source, subpath=args.subpath, name=args.name)) + elif args.command == "install": + _emit( + _install_source( + workspace, + args.source, + subpath=args.subpath, + name=args.name, + force=args.force, + ) + ) + elif args.command == "remove": + _emit(_remove(workspace, args.name, confirmed=args.yes)) + elif args.command == "restore": + _emit(_restore(workspace, args.trash_name)) + elif args.command == "skillhub-search": + _skillhub_search(args.query, args.limit) + else: + raise SkillManagerError(f"unknown command: {args.command}") + except SkillManagerError as exc: + _emit({"error": str(exc)}) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/octop/infra/agents/experts/catalog.py b/src/octop/infra/agents/experts/catalog.py index 93e889d1..0468b6c2 100644 --- a/src/octop/infra/agents/experts/catalog.py +++ b/src/octop/infra/agents/experts/catalog.py @@ -84,12 +84,55 @@ def discover_seed_paths(expert_dir: Path) -> list[str]: def preview_file_paths(expert: Expert) -> list[str]: - """Paths shown in the create-from-expert drawer: ``prompt_files`` + ``skills/``.""" - paths = list(expert.prompt_files) - paths.extend(f for f in expert.files if f.startswith("skills/")) + """Paths shown in the create-from-expert drawer: persona md, skills/, agents/.""" + return preview_paths_from_inventory(expert.prompt_files, expert.files) + + +def preview_paths_from_inventory( + prompt_files: list[str], + seed_paths: list[str], +) -> list[str]: + """Build dashboard preview paths from manifest ``prompt_files`` + skills/agents.""" + paths: list[str] = [] + seen: set[str] = set() + for rel in prompt_files: + if rel in seen or rel == MANIFEST_FILENAME or not _is_prompt_md_path(rel): + continue + paths.append(rel) + seen.add(rel) + for rel in sorted(seed_paths): + if rel.startswith("skills/") and rel not in seen: + paths.append(rel) + seen.add(rel) + for rel in sorted(seed_paths): + if _is_subagent_path(rel) and rel not in seen: + paths.append(rel) + seen.add(rel) return paths +def preview_paths_from_expert_dir(expert_dir: Path) -> list[str]: + """Preview paths for a snapshot directory (published experts).""" + manifest_path = expert_dir / MANIFEST_FILENAME + prompt_files: list[str] = [] + if manifest_path.is_file(): + data = _read_manifest(manifest_path) + if isinstance(data, dict): + raw = data.get("prompt_files") + if isinstance(raw, list): + prompt_files = [str(item) for item in raw if str(item).strip()] + return preview_paths_from_inventory(prompt_files, discover_seed_paths(expert_dir)) + + +def _is_subagent_path(rel: str) -> bool: + return rel.startswith("agents/") and rel.endswith(".md") + + +def _is_prompt_md_path(rel: str) -> bool: + """Workspace-root persona markdown listed in ``prompt_files``.""" + return "/" not in rel and rel.endswith(".md") + + async def seed_expert_directory( *, expert_dir: Path, diff --git a/src/octop/infra/agents/experts/market_creation.py b/src/octop/infra/agents/experts/market_creation.py index d0d99973..c5626977 100644 --- a/src/octop/infra/agents/experts/market_creation.py +++ b/src/octop/infra/agents/experts/market_creation.py @@ -43,6 +43,7 @@ class SkillHubMarketAgentCreateOptions: providers: list[str] | None = None default_model: str | None = None backend: dict[str, Any] | None = None + color: str | None = None max_iters: int | None = None max_input_length: int | None = None temperature: float | None = None @@ -279,6 +280,8 @@ async def create_agent_from_skillhub_skillset( config_extra["providers"] = list(options.providers) if options.backend: config_extra["backend"] = options.backend + if options.color: + config_extra["color"] = options.color locale = resolve_user_locale( user_repo=server.services.user_repo, @@ -322,7 +325,7 @@ async def create_agent_from_skillhub_skillset( row=row, expert_id=item.expert_id, icon_name=expert.summary.icon_name, - color=expert.summary.color, + color=options.color or expert.summary.color, slug=item.slug, welcome_enrichment=welcome_enrichment, ) diff --git a/src/octop/infra/agents/experts/publish.py b/src/octop/infra/agents/experts/publish.py new file mode 100644 index 00000000..20227590 --- /dev/null +++ b/src/octop/infra/agents/experts/publish.py @@ -0,0 +1,263 @@ +"""Export agent workspaces as installable published-expert snapshots.""" + +from __future__ import annotations + +import json +import os +import shutil +import tempfile +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Any +from uuid import uuid4 + +from harness_agent.backends.workspace import BackendWorkspace + +from octop.infra.agents.builtin_skills import OCTOP_BUILTIN_SKILLS_ROOT +from octop.infra.agents.experts.catalog import MANIFEST_FILENAME +from octop.infra.agents.subagents.catalog import slugify +from octop.infra.db.repos.published_experts import PublishedExpertRepo, PublishedExpertRow +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.identity import User + +_EXCLUDED_PARTS = frozenset( + { + ".git", + "__pycache__", + ".harness-materialize", + OCTOP_BUILTIN_SKILLS_ROOT, + # Runtime / user-session data — never seed into published templates. + "inbound", + "daily", + "uploads", + "sessions", + "media", + "logs", + } +) +_EXCLUDED_FILENAMES = frozenset({".env", "credentials.json"}) +_MEMORY_SQLITE_PREFIX = "memory.sqlite" +_EXPORT_ROOT_MD = frozenset( + { + "SOUL.md", + "IDENTITY.md", + "USER.md", + "AGENTS.md", + "TOOLS.md", + "HEARTBEAT.md", + "PROACTIVE.md", + } +) +_SEEDABLE_GLOB_PATTERNS = ("*.md", "skills/**/*", "agents/*.md") + + +@dataclass(frozen=True) +class PublishedExpertSnapshotMeta: + name: str + description: str + icon_name: str | None + color: str | None + label_zh: str + label_en: str + welcome_message_zh: str + welcome_message_en: str + quick_prompts: tuple[dict[str, Any], ...] = () + + +def assert_can_mutate_published(row: PublishedExpertRow, user: User) -> None: + """Allow a published expert's creator or an administrator to mutate it.""" + if user.is_admin or str(user.id) == row.created_by: + return + raise OctopError(ErrorCode.FORBIDDEN, "published expert can only be modified by its creator") + + +def resolve_published_expert_slug( + *, + repo: PublishedExpertRepo, + name: str, + slug: str | None = None, +) -> str: + """Return a unique derived slug or reject an already-requested explicit slug.""" + requested_slug = (slug or "").strip() + requested = requested_slug or slugify(name) or "expert" + if requested_slug: + if repo.get_by_slug(requested) is not None: + raise OctopError.localized( + ErrorCode.PUBLISHED_EXPERT_SLUG_TAKEN, + slug=requested, + details={"slug": requested}, + ) + return requested + + candidate = requested + suffix = 2 + while repo.get_by_slug(candidate) is not None: + candidate = f"{requested}-{suffix}" + suffix += 1 + return candidate + + +async def export_agent_workspace_to_dir( + *, + workspace: BackendWorkspace, + dest: Path, + metadata: PublishedExpertSnapshotMeta | None = None, + manifest_id: str | None = None, +) -> list[str]: + """Atomically replace *dest* with exported workspace files.""" + dest.parent.mkdir(parents=True, exist_ok=True) + staging_dir = Path(tempfile.mkdtemp(prefix=f".{dest.name}.", dir=dest.parent)) + try: + exported = await _write_workspace_snapshot( + workspace=workspace, + dest=staging_dir, + metadata=metadata, + manifest_id=manifest_id or dest.name, + ) + _replace_snapshot_dir(staging_dir, dest) + except BaseException: + shutil.rmtree(staging_dir, ignore_errors=True) + raise + return exported + + +async def _write_workspace_snapshot( + *, + workspace: BackendWorkspace, + dest: Path, + metadata: PublishedExpertSnapshotMeta | None, + manifest_id: str, +) -> list[str]: + """Copy seedable workspace files into an empty staging directory.""" + paths = await _workspace_file_paths(workspace) + dest.mkdir(parents=True, exist_ok=True) + + exported: list[str] = [] + for rel in paths: + if not _is_seedable_path(rel): + continue + if rel == MANIFEST_FILENAME: + continue + content = await workspace.adownload_bytes(rel) + if content is None: + continue + target = dest.joinpath(*PurePosixPath(rel).parts) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + exported.append(rel) + + prompt_files = sorted(rel for rel in exported if "/" not in rel and rel.endswith(".md")) + if metadata is not None: + manifest = _manifest_from_metadata( + metadata, + manifest_id=manifest_id, + prompt_files=prompt_files, + ) + else: + raw_manifest = await workspace.adownload_bytes(MANIFEST_FILENAME) + if raw_manifest is None: + raise ValueError(f"workspace does not contain {MANIFEST_FILENAME}") + _validate_manifest(raw_manifest) + manifest = raw_manifest + + manifest_path = dest / MANIFEST_FILENAME + manifest_path.write_bytes(manifest) + exported.append(MANIFEST_FILENAME) + return sorted(exported) + + +def _replace_snapshot_dir(staging_dir: Path, dest: Path) -> None: + """Swap a completed staging directory into place, restoring on failure.""" + backup_dir = dest.with_name(f".{dest.name}.previous-{uuid4().hex}") + + moved_existing = dest.exists() + if moved_existing: + os.replace(dest, backup_dir) + try: + os.replace(staging_dir, dest) + except BaseException: + if moved_existing and not dest.exists(): + os.replace(backup_dir, dest) + raise + if moved_existing: + shutil.rmtree(backup_dir, ignore_errors=True) + + +def _manifest_from_metadata( + metadata: PublishedExpertSnapshotMeta, + *, + manifest_id: str, + prompt_files: list[str] | None = None, +) -> bytes: + """Build snapshot manifest from publish metadata only — never copy workspace manifest.""" + data: dict[str, Any] = { + "id": manifest_id, + "label": {"zh": metadata.label_zh, "en": metadata.label_en}, + "description": {"zh": metadata.description, "en": metadata.description}, + "welcome_message": { + "zh": metadata.welcome_message_zh, + "en": metadata.welcome_message_en, + }, + } + if prompt_files: + data["prompt_files"] = prompt_files + if metadata.icon_name: + data["icon_name"] = metadata.icon_name + if metadata.color: + data["color"] = metadata.color + if metadata.quick_prompts: + data["quick_prompts"] = list(metadata.quick_prompts) + return json.dumps(data, ensure_ascii=False).encode("utf-8") + + +def _validate_manifest(content: bytes | None) -> None: + if content is None: + raise ValueError(f"workspace does not contain {MANIFEST_FILENAME}") + try: + data = json.loads(content) + except (json.JSONDecodeError, UnicodeDecodeError) as exc: + raise ValueError(f"{MANIFEST_FILENAME} must be a valid JSON object") from exc + if not isinstance(data, dict): + raise ValueError(f"{MANIFEST_FILENAME} must be a valid JSON object") + + +async def _workspace_file_paths(workspace: BackendWorkspace) -> list[str]: + """List candidate workspace files for expert-template export.""" + paths: set[str] = set() + for pattern in _SEEDABLE_GLOB_PATTERNS: + result = await workspace.aglob(pattern) + if result is None: + raise RuntimeError("workspace backend cannot list files for snapshot export") + for entry in result.matches or []: + if isinstance(entry, dict): + is_dir = bool(entry.get("is_dir", False)) + path = entry.get("path") + else: + is_dir = bool(getattr(entry, "is_dir", False)) + path = getattr(entry, "path", None) + if is_dir: + continue + if not isinstance(path, str): + continue + rel = path.replace("\\", "/").lstrip("/") + if rel and rel != ".": + paths.add(rel) + return sorted(paths) + + +def _is_seedable_path(path: str) -> bool: + parts = PurePosixPath(path).parts + if not parts or any(part in _EXCLUDED_PARTS or part.startswith(".") for part in parts): + return False + basename = parts[-1] + if basename in _EXCLUDED_FILENAMES: + return False + if basename.startswith(_MEMORY_SQLITE_PREFIX): + return False + if len(parts) == 1: + return basename in _EXPORT_ROOT_MD + if parts[0] == "skills": + return True + if parts[0] == "agents": + return basename.endswith(".md") + return False diff --git a/src/octop/infra/agents/experts/published_creation.py b/src/octop/infra/agents/experts/published_creation.py new file mode 100644 index 00000000..36d07de9 --- /dev/null +++ b/src/octop/infra/agents/experts/published_creation.py @@ -0,0 +1,312 @@ +"""Publish / refresh / install / unpublish user expert templates.""" + +from __future__ import annotations + +import asyncio +import json +import shutil +from dataclasses import dataclass +from pathlib import Path +from sqlite3 import IntegrityError as SqliteIntegrityError +from typing import Any, cast + +from psycopg import IntegrityError as PsycopgIntegrityError + +from octop.infra.agents.experts.catalog import MANIFEST_FILENAME, seed_expert_directory +from octop.infra.agents.experts.publish import ( + PublishedExpertSnapshotMeta, + assert_can_mutate_published, + export_agent_workspace_to_dir, + resolve_published_expert_slug, +) +from octop.infra.agents.manager import AgentCreateSpec +from octop.infra.db.repos.published_experts import PublishedExpertRow +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.identity import User +from octop.infra.utils.ulid import new_ulid + + +@dataclass(frozen=True) +class PublishedExpertInstallOptions: + name: str + description: str = "" + providers: list[str] | None = None + default_model: str | None = None + backend: dict[str, Any] | None = None + skill_package_ids: list[str] | None = None + color: str | None = None + runtime_config: dict[str, Any] | None = None + + +def _snapshot_dir(services: Any, expert_id: str) -> Path: + return Path(services.paths.published_experts_dir) / expert_id + + +def _read_snapshot_manifest(snapshot_dir: Path) -> dict[str, Any]: + manifest_path = snapshot_dir / MANIFEST_FILENAME + if not manifest_path.is_file(): + return {} + try: + data = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + return data if isinstance(data, dict) else {} + + +def _localized_text(node: Any, *, lang: str) -> str: + if isinstance(node, dict): + return str(node.get(lang) or node.get("zh" if lang == "zh" else "en") or "") + if isinstance(node, str): + return node + return "" + + +def _manifest_welcome(manifest: dict[str, Any]) -> tuple[str, str]: + welcome = manifest.get("welcome_message") + return _localized_text(welcome, lang="zh"), _localized_text(welcome, lang="en") + + +def _manifest_quick_prompts(manifest: dict[str, Any]) -> tuple[dict[str, Any], ...]: + raw = manifest.get("quick_prompts") + if not isinstance(raw, list): + return () + out: list[dict[str, Any]] = [] + for item in raw: + if isinstance(item, dict): + out.append(item) + return tuple(out) + + +def _agent_color(registry: Any, agent_id: str) -> str | None: + cfg = registry.get_config(agent_id) + color = cfg.get("color") + return str(color).strip() if isinstance(color, str) and color.strip() else None + + +def _snapshot_meta( + source: Any, + *, + name: str, + description: str, + color: str | None, + welcome_message_zh: str = "", + welcome_message_en: str = "", + quick_prompts: tuple[dict[str, Any], ...] = (), +) -> PublishedExpertSnapshotMeta: + return PublishedExpertSnapshotMeta( + name=name, + description=description, + icon_name=(source.icon or None), + color=color, + label_zh=name, + label_en=name, + welcome_message_zh=welcome_message_zh, + welcome_message_en=welcome_message_en, + quick_prompts=quick_prompts, + ) + + +def require_published_expert(services: Any, expert_id: str) -> PublishedExpertRow: + row = services.published_expert_repo.get(expert_id) + if row is None: + raise OctopError(ErrorCode.NOT_FOUND, f"published expert {expert_id!r} not found") + return cast(PublishedExpertRow, row) + + +async def publish_agent_expert( + *, + services: Any, + registry: Any, + user: User, + source: Any, + workspace: Any, + name: str, + description: str = "", + slug: str | None = None, + welcome_message_zh: str = "", + welcome_message_en: str = "", + quick_prompts: tuple[dict[str, Any], ...] = (), +) -> PublishedExpertRow: + """Snapshot an owned agent workspace into a globally installable expert template.""" + repo = services.published_expert_repo + existing = repo.get_by_source_agent_id(source.agent_id) + if existing is not None: + raise OctopError.localized( + ErrorCode.PUBLISHED_EXPERT_ALREADY_EXISTS, + name=existing.name, + details={"id": existing.id, "slug": existing.slug}, + ) + + resolved_slug = resolve_published_expert_slug(repo=repo, name=name, slug=slug) + expert_id = new_ulid() + snapshot_dir = _snapshot_dir(services, expert_id) + resolved_description = description or source.description or "" + color = _agent_color(registry, source.agent_id) or "" + icon_name = source.icon or "" + try: + await export_agent_workspace_to_dir( + workspace=workspace, + dest=snapshot_dir, + metadata=_snapshot_meta( + source, + name=name, + description=resolved_description, + color=color or None, + welcome_message_zh=welcome_message_zh, + welcome_message_en=welcome_message_en, + quick_prompts=quick_prompts, + ), + manifest_id=resolved_slug, + ) + return cast( + PublishedExpertRow, + repo.create( + id=expert_id, + slug=resolved_slug, + name=name, + description=resolved_description, + created_by=str(user.id), + source_agent_id=source.agent_id, + icon_name=icon_name, + color=color, + ), + ) + except (SqliteIntegrityError, PsycopgIntegrityError) as exc: + await asyncio.to_thread(shutil.rmtree, snapshot_dir, ignore_errors=True) + raise OctopError.localized( + ErrorCode.PUBLISHED_EXPERT_SLUG_TAKEN, + slug=resolved_slug, + details={"slug": resolved_slug}, + ) from exc + except Exception: + await asyncio.to_thread(shutil.rmtree, snapshot_dir, ignore_errors=True) + raise + + +async def refresh_published_expert( + *, + services: Any, + registry: Any, + user: User, + expert_id: str, + source: Any, + workspace: Any, + name: str | None = None, + description: str | None = None, + welcome_message_zh: str | None = None, + welcome_message_en: str | None = None, + quick_prompts: tuple[dict[str, Any], ...] | None = None, +) -> PublishedExpertRow: + """Replace a published snapshot using its still-owned source agent workspace.""" + row = require_published_expert(services, expert_id) + assert_can_mutate_published(row, user) + if row.source_agent_id is None: + raise OctopError(ErrorCode.NOT_FOUND, "published expert source agent not found") + + color = _agent_color(registry, source.agent_id) or "" + icon_name = source.icon or "" + snapshot_dir = _snapshot_dir(services, row.id) + existing_manifest = await asyncio.to_thread(_read_snapshot_manifest, snapshot_dir) + existing_welcome_zh, existing_welcome_en = _manifest_welcome(existing_manifest) + existing_quick_prompts = _manifest_quick_prompts(existing_manifest) + resolved_name = name if name is not None else row.name + resolved_description = description if description is not None else row.description + resolved_welcome_zh = ( + welcome_message_zh if welcome_message_zh is not None else existing_welcome_zh + ) + resolved_welcome_en = ( + welcome_message_en if welcome_message_en is not None else existing_welcome_en + ) + resolved_quick_prompts = quick_prompts if quick_prompts is not None else existing_quick_prompts + await export_agent_workspace_to_dir( + workspace=workspace, + dest=snapshot_dir, + metadata=_snapshot_meta( + source, + name=resolved_name, + description=resolved_description, + color=color or None, + welcome_message_zh=resolved_welcome_zh, + welcome_message_en=resolved_welcome_en, + quick_prompts=resolved_quick_prompts, + ), + manifest_id=row.slug, + ) + return cast( + PublishedExpertRow, + services.published_expert_repo.update_snapshot_meta( + row.id, + icon_name=icon_name, + color=color, + name=resolved_name, + description=resolved_description, + ), + ) + + +async def unpublish_expert(*, services: Any, user: User, expert_id: str) -> None: + """Remove a published expert's listing and snapshot without deleting installed forks.""" + row = require_published_expert(services, expert_id) + assert_can_mutate_published(row, user) + services.published_expert_repo.delete(row.id) + await asyncio.to_thread(shutil.rmtree, _snapshot_dir(services, row.id), ignore_errors=True) + + +async def install_published_expert( + *, + services: Any, + registry: Any, + user: User, + expert_id: str, + options: PublishedExpertInstallOptions, +) -> dict[str, Any]: + """Create a private agent and seed it from the immutable published snapshot.""" + row = require_published_expert(services, expert_id) + snapshot_dir = _snapshot_dir(services, row.id) + if not snapshot_dir.is_dir(): + raise OctopError(ErrorCode.NOT_FOUND, f"published expert {expert_id!r} snapshot not found") + + package_ids = ( + registry.validate_skill_package_ids(options.skill_package_ids) + if options.skill_package_ids is not None + else None + ) + if package_ids: + registry.assert_backend_supports_skill_packages(options.backend) + + config_extra: dict[str, Any] = {"published_expert_id": row.id} + if options.providers: + config_extra["providers"] = list(options.providers) + if options.backend: + config_extra["backend"] = options.backend + if options.color: + config_extra["color"] = options.color + + created = await registry.create( + AgentCreateSpec( + name=options.name, + user_id=user.id, + description=options.description or row.description, + default_model=options.default_model, + runtime_config=options.runtime_config or {}, + config=config_extra, + ) + ) + workspace = registry.workspace_for_agent(created.agent_id) + if workspace is None: + raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {created.agent_id!r} not found") + await seed_expert_directory(expert_dir=snapshot_dir, workspace=workspace) + if package_ids is not None: + await registry.persist_skill_package_ids(created.agent_id, package_ids) + await registry.reload(created.agent_id) + return { + "id": created.id, + "agent_id": created.agent_id, + "user_id": created.user_id, + "name": created.name, + "description": created.description, + "default_model": created.default_model, + "state": created.last_state or "unknown", + "published_expert_id": row.id, + "bootstrap_pending": not registry.is_bootstrapped(created.agent_id), + } diff --git a/src/octop/infra/agents/manager.py b/src/octop/infra/agents/manager.py index e046dbbd..71da880c 100644 --- a/src/octop/infra/agents/manager.py +++ b/src/octop/infra/agents/manager.py @@ -184,6 +184,31 @@ def _resolve_memory_backend_kwargs( return memory_backend_from_agent_config(cfg, octop_config=config, workspace_dir=workspace_dir) +async def _fill_missing_subagent_colors(agent: Any, rows: list[dict[str, Any]]) -> None: + """Copy ``color`` from workspace frontmatter when harness omitted it.""" + missing = [row for row in rows if not str(row.get("color") or "").strip() and row.get("path")] + if not missing: + return + workspace = getattr(agent, "workspace", None) + aread = getattr(workspace, "aread_text", None) + if not callable(aread): + return + from octop.infra.utils.frontmatter import parse_frontmatter + + for row in missing: + path = str(row.get("path") or "") + try: + text = await aread(path) + except (OSError, TypeError, ValueError): + continue + if not isinstance(text, str) or not text: + continue + meta, _body = parse_frontmatter(text) + color = str(meta.get("color") or "").strip() + if color: + row["color"] = color + + # --------------------------------------------------------------------------- # Data types # --------------------------------------------------------------------------- @@ -202,6 +227,7 @@ class AgentCreateSpec: system_prompt: str | None = None icon: str | None = None template_name: str | None = None + is_shared: bool = False runtime_config: dict[str, Any] = field(default_factory=dict) config: dict[str, Any] = field(default_factory=dict) @@ -415,6 +441,10 @@ async def create(self, spec: AgentCreateSpec, *, defer_bootstrap: bool = False) ) row = self._repos.agent_repo.get(agent_id) assert row is not None + if spec.is_shared: + self._repos.agent_repo.set_shared(agent_id, True) + row = self._repos.agent_repo.get(agent_id) + assert row is not None if spec.template_name: await self._seed_expert_template(row, spec.template_name) if defer_bootstrap: @@ -473,6 +503,14 @@ async def update(self, agent_id: str, **kwargs: Any) -> AgentRow: self._schedule_reload(agent_id) return row + async def set_shared(self, agent_id: str, shared: bool) -> AgentRow: + """Persist whether other users may access this agent.""" + self._repos.agent_repo.set_shared(agent_id, shared) + row = self._repos.agent_repo.get(agent_id) + if row is None: + raise OctopError(ErrorCode.AGENT_NOT_FOUND, f"agent {agent_id!r} not found") + return row + async def delete(self, agent_id: str) -> None: """Remove agent from DB, harness runtime, and workspace directory.""" async with self._lock: @@ -1467,7 +1505,9 @@ async def list_skill_summaries(self, agent_id: str) -> list[dict[str, Any]]: async def list_subagent_summaries(self, agent_id: str) -> list[dict[str, Any]]: """Installed subagents for *agent_id* (delegates to harness-agent catalog).""" agent = self.get_agent(agent_id) - return await agent.list_subagent_summaries() + rows = [dict(row) for row in await agent.list_subagent_summaries()] + await _fill_missing_subagent_colors(agent, rows) + return rows def sync_skills_disabled(self, agent_id: str, disabled: set[str]) -> None: """Push ``skills_disabled`` to the running harness agent (hot update).""" @@ -1567,6 +1607,23 @@ async def _post_start_agent( sorted(tool_set)[:8], ) ws = agent.workspace + try: + from octop.infra.agents.builtin_skills import ( # noqa: PLC0415 + sync_octop_builtin_skills, + ) + + synced_skills = await sync_octop_builtin_skills(ws) + logger.info( + "Agent %s: synced Octop built-in skills=%s", + row.agent_id, + synced_skills, + ) + except Exception: + logger.warning( + "Agent %s: failed to sync Octop built-in skills", + row.agent_id, + exc_info=True, + ) if self._plugin_manager is not None: await asyncio.to_thread(self._plugin_manager.sync_skills_to_workspace, ws) @@ -1857,6 +1914,18 @@ def _build_harness_config(self, row: AgentRow) -> HarnessAgentConfig: cron_tools = build_cronjob_tools(self._cron_manager) + from types import SimpleNamespace # noqa: PLC0415 + + from octop.infra.knowledge.tools import build_knowledge_tools # noqa: PLC0415 + + knowledge_tools = build_knowledge_tools( + SimpleNamespace( + knowledge_repo=self._repos.knowledge_repo, + settings_repo=self._repos.settings_repo, + provider_repo=self._repos.provider_repo, + ) + ) + from harness_agent.plugins import PluginRegistry, build_plugin_tools # noqa: PLC0415 agent_plugins = cfg.get("plugins") if isinstance(cfg.get("plugins"), dict) else {} @@ -1874,18 +1943,21 @@ def _build_harness_config(self, row: AgentRow) -> HarnessAgentConfig: from octop.infra.agents.middleware.binary_read_guard import BinaryReadGuardMiddleware from octop.infra.agents.middleware.reasoning import ReasoningRequestMiddleware + from octop.infra.knowledge.hint import KnowledgeSearchHintMiddleware # FilesystemGuard + ModelSettings live in harness-agent (auto-mounted). # BinaryReadGuard stays Octop-specific (inbound/attachment product policy). agent_middleware: list[Any] = [ *plugin_middleware, ReasoningRequestMiddleware(), + KnowledgeSearchHintMiddleware(), BinaryReadGuardMiddleware(), ] merged_tools: list[Any] = [] if cron_tools: merged_tools.extend(cron_tools) + merged_tools.extend(knowledge_tools) merged_tools.extend(plugin_tools) if self._harness_manager is not None: merged_tools.extend(self._harness_manager.team.team_tools()) diff --git a/src/octop/infra/agents/providers/model_flags.py b/src/octop/infra/agents/providers/model_flags.py new file mode 100644 index 00000000..9c0f2a0f --- /dev/null +++ b/src/octop/infra/agents/providers/model_flags.py @@ -0,0 +1,105 @@ +"""Flags on provider ``models_json`` entries that affect chat / auto-routing.""" + +from __future__ import annotations + +from typing import Any + +# LocalServiceCard creates the ONNX provider with ``api_key=preset.id`` ("onnx"). +_ONNX_PRESET_API_KEY = "onnx" +# Exact display / id names from the onnx preset — never substring "local"/"onnx". +_ONNX_PRESET_NAMES = frozenset({"onnx", "onnx (local)"}) +# Same pattern for Ollama (placeholder api_key + exact preset names). +_OLLAMA_PRESET_API_KEY = "ollama" +_OLLAMA_PRESET_NAMES = frozenset({"ollama", "ollama (local)"}) + + +def is_onnx_local_provider( + provider_name: str | None = None, + *, + provider_api_key: str | None = None, +) -> bool: + """True when this row is the local ONNX embedding preset. + + Prefer the stable placeholder API key written at create time. Fall back to + exact preset name/id matches only — Ollama is also "local" and must not match. + """ + if provider_api_key is not None and provider_api_key.strip().lower() == _ONNX_PRESET_API_KEY: + return True + if not provider_name: + return False + return provider_name.strip().lower() in _ONNX_PRESET_NAMES + + +def is_ollama_local_provider( + provider_name: str | None = None, + *, + provider_api_key: str | None = None, + provider_base_url: str | None = None, +) -> bool: + """True when this row is the local Ollama preset. + + Prefer the stable placeholder API key, then exact preset names. Base URL + hints match the dashboard (port 11434 / host ``ollama``) so delete + protection stays aligned with the hidden UI button. + """ + if is_onnx_local_provider(provider_name, provider_api_key=provider_api_key): + return False + if provider_api_key is not None and provider_api_key.strip().lower() == _OLLAMA_PRESET_API_KEY: + return True + if provider_name and provider_name.strip().lower() in _OLLAMA_PRESET_NAMES: + return True + url = (provider_base_url or "").strip().lower() + return "11434" in url or "ollama" in url + + +def is_local_runtime_provider( + provider_name: str | None = None, + *, + provider_api_key: str | None = None, + provider_base_url: str | None = None, +) -> bool: + """True for onboard local runtimes (ONNX / Ollama) that must not be deleted.""" + return is_onnx_local_provider( + provider_name, provider_api_key=provider_api_key + ) or is_ollama_local_provider( + provider_name, + provider_api_key=provider_api_key, + provider_base_url=provider_base_url, + ) + + +def is_embedding_model( + model: dict[str, Any], + *, + provider_name: str | None = None, + provider_api_key: str | None = None, +) -> bool: + """True when *model* is embedding-only (not for chat / auto-route). + + Explicit flags win: + - ``embedding: true`` + - ``task: "embedding"`` + + Legacy fallback: models under the ONNX local provider are treated as + embedding-only even if the flag was missing. + """ + if model.get("embedding") is True: + return True + task = str(model.get("task") or "").strip().lower() + if task == "embedding": + return True + return is_onnx_local_provider(provider_name, provider_api_key=provider_api_key) + + +def is_chat_eligible_model( + model: dict[str, Any], + *, + provider_name: str | None = None, + provider_api_key: str | None = None, +) -> bool: + """True when *model* may appear in chat pickers and auto-route candidates.""" + if not model.get("enabled", True): + return False + if is_embedding_model(model, provider_name=provider_name, provider_api_key=provider_api_key): + return False + return bool(str(model.get("id") or "").strip()) diff --git a/src/octop/infra/agents/providers/onnx_catalog.py b/src/octop/infra/agents/providers/onnx_catalog.py new file mode 100644 index 00000000..ee75d74a --- /dev/null +++ b/src/octop/infra/agents/providers/onnx_catalog.py @@ -0,0 +1,180 @@ +"""Curated ONNX / fastembed embedding model catalog.""" + +from __future__ import annotations + +from typing import Any + +# Finnie Memory page defaults (recommended). +ONNX_PRESET_MODEL_IDS: tuple[str, ...] = ( + "BAAI/bge-small-zh-v1.5", + "jinaai/jina-embeddings-v2-base-zh", + "intfloat/multilingual-e5-large", +) + +_EXTRA_CATALOG_IDS: tuple[str, ...] = ( + "BAAI/bge-small-en-v1.5", + "BAAI/bge-base-en-v1.5", + "BAAI/bge-large-en-v1.5", + "BAAI/bge-base-zh-v1.5", + "jinaai/jina-embeddings-v2-base-en", + "jinaai/jina-embeddings-v2-small-en", + "intfloat/multilingual-e5-base", + "intfloat/multilingual-e5-small", + "sentence-transformers/all-MiniLM-L6-v2", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + "thenlper/gte-base", + "thenlper/gte-large", +) + +# Approximate sizes (GB) when fastembed metadata is unavailable. +_FALLBACK_SIZE_GB: dict[str, float] = { + "BAAI/bge-small-zh-v1.5": 0.09, + "jinaai/jina-embeddings-v2-base-zh": 0.32, + "intfloat/multilingual-e5-large": 1.2, + "BAAI/bge-small-en-v1.5": 0.13, + "BAAI/bge-base-en-v1.5": 0.21, + "BAAI/bge-large-en-v1.5": 0.53, + "BAAI/bge-base-zh-v1.5": 0.4, + "jinaai/jina-embeddings-v2-base-en": 0.32, + "jinaai/jina-embeddings-v2-small-en": 0.13, + "intfloat/multilingual-e5-base": 0.44, + "intfloat/multilingual-e5-small": 0.13, + "sentence-transformers/all-MiniLM-L6-v2": 0.09, + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2": 0.12, + "thenlper/gte-base": 0.22, + "thenlper/gte-large": 0.67, +} + +# When fastembed is not installed, still recognize HF cache dirs it would create. +_HF_SOURCE_FALLBACK: dict[str, str] = { + "BAAI/bge-small-zh-v1.5": "Qdrant/bge-small-zh-v1.5", + "BAAI/bge-small-en-v1.5": "qdrant/bge-small-en-v1.5-onnx-q", + "BAAI/bge-base-en-v1.5": "qdrant/bge-base-en-v1.5-onnx-q", + "BAAI/bge-large-en-v1.5": "qdrant/bge-large-en-v1.5-onnx", + "sentence-transformers/all-MiniLM-L6-v2": "qdrant/all-MiniLM-L6-v2-onnx", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2": ( + "qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q" + ), + "jinaai/jina-embeddings-v2-base-en": "xenova/jina-embeddings-v2-base-en", + "jinaai/jina-embeddings-v2-small-en": "xenova/jina-embeddings-v2-small-en", + "thenlper/gte-large": "qdrant/gte-large-onnx", + "intfloat/multilingual-e5-large": "qdrant/multilingual-e5-large-onnx", +} + + +def _model_entry( + model_id: str, + *, + recommended: bool = False, + size_gb: float | None = None, + hf_source: str | None = None, +) -> dict[str, Any]: + entry: dict[str, Any] = { + "id": model_id, + "name": model_id, + "recommended": recommended, + } + if size_gb is not None: + entry["size_gb"] = size_gb + if hf_source: + entry["hf_source"] = hf_source + return entry + + +def _fastembed_meta_map() -> dict[str, dict[str, Any]]: + try: + from fastembed import TextEmbedding + except ImportError: + return {} + try: + raw = TextEmbedding.list_supported_models() + except Exception: + return {} + out: dict[str, dict[str, Any]] = {} + for item in raw or []: + if not isinstance(item, dict): + continue + model = item.get("model") or item.get("model_name") or item.get("id") + if not isinstance(model, str) or not model: + continue + out[model] = item + return out + + +def get_onnx_model_meta(model_id: str) -> dict[str, Any]: + """Return size / HF source metadata for a model id.""" + meta = _fastembed_meta_map().get(model_id) or {} + size = meta.get("size_in_GB") + size_gb: float | None + try: + size_gb = float(size) if size is not None else None + except (TypeError, ValueError): + size_gb = None + if size_gb is None: + size_gb = _FALLBACK_SIZE_GB.get(model_id) + sources = meta.get("sources") if isinstance(meta.get("sources"), dict) else {} + hf_source = sources.get("hf") if isinstance(sources, dict) else None + if not isinstance(hf_source, str) or not hf_source.strip(): + hf_source = _HF_SOURCE_FALLBACK.get(model_id) + return { + "id": model_id, + "size_gb": size_gb, + "hf_source": hf_source, + "supported": bool(meta), + } + + +def list_onnx_catalog_models() -> list[dict[str, Any]]: + """Return catalog entries for the Models local ONNX panel.""" + meta_map = _fastembed_meta_map() + recommended = set(ONNX_PRESET_MODEL_IDS) + + def enrich(model_id: str, *, rec: bool) -> dict[str, Any]: + info = get_onnx_model_meta(model_id) if not meta_map else None + if meta_map and model_id in meta_map: + item = meta_map[model_id] + size = item.get("size_in_GB") + try: + size_gb = float(size) if size is not None else _FALLBACK_SIZE_GB.get(model_id) + except (TypeError, ValueError): + size_gb = _FALLBACK_SIZE_GB.get(model_id) + sources = item.get("sources") if isinstance(item.get("sources"), dict) else {} + hf = sources.get("hf") if isinstance(sources, dict) else None + if not isinstance(hf, str) or not hf.strip(): + hf = _HF_SOURCE_FALLBACK.get(model_id) + return _model_entry( + model_id, + recommended=rec, + size_gb=size_gb, + hf_source=hf, + ) + if info: + return _model_entry( + model_id, + recommended=rec, + size_gb=info.get("size_gb"), + hf_source=info.get("hf_source"), + ) + return _model_entry( + model_id, + recommended=rec, + size_gb=_FALLBACK_SIZE_GB.get(model_id), + ) + + if meta_map: + ordered = list(ONNX_PRESET_MODEL_IDS) + sorted( + mid for mid in meta_map if mid not in recommended + ) + return [enrich(mid, rec=mid in recommended) for mid in ordered] + + seen: set[str] = set() + out: list[dict[str, Any]] = [] + for mid in ONNX_PRESET_MODEL_IDS: + seen.add(mid) + out.append(enrich(mid, rec=True)) + for mid in _EXTRA_CATALOG_IDS: + if mid in seen: + continue + seen.add(mid) + out.append(enrich(mid, rec=False)) + return out diff --git a/src/octop/infra/agents/providers/onnx_service.py b/src/octop/infra/agents/providers/onnx_service.py new file mode 100644 index 00000000..87653fa0 --- /dev/null +++ b/src/octop/infra/agents/providers/onnx_service.py @@ -0,0 +1,569 @@ +"""ONNX local embedding service: config, cache, and download lifecycle. + +This is **not** a chat Provider. It prepares local ONNX / fastembed embedding +models under ``~/.octop/embedding_models`` for the Models admin local tab. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +import re +import shutil +import threading +import time +import uuid +from collections.abc import Sequence +from dataclasses import dataclass, field +from enum import StrEnum +from pathlib import Path +from typing import Any + +from octop.infra.agents.providers.onnx_catalog import ( + ONNX_PRESET_MODEL_IDS, + get_onnx_model_meta, + list_onnx_catalog_models, +) +from octop.infra.utils.paths import PathLayout +from octop.infra.utils.runtime_packages import ( + PackageInstallSpec, + install_packages, + purge_import_cache, +) + +logger = logging.getLogger(__name__) + +_SETTINGS_KEY = "onnx_local_service" +_DEFAULT_MODEL = ONNX_PRESET_MODEL_IDS[0] +# Fixed allowlist — never take package names from user input. +_LOCAL_EMBEDDING_PIP_SPECS: tuple[str, ...] = ( + "fastembed>=0.4", + "huggingface_hub>=0.20", +) +_LOCAL_EMBEDDING_SPEC = PackageInstallSpec( + packages=_LOCAL_EMBEDDING_PIP_SPECS, + extra_fallback="local-embedding", +) +_MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*$") + + +def runtime_pip_allowed() -> bool: + """Runtime pip is off by default; set OCTOP_ALLOW_RUNTIME_PIP=1 to enable.""" + return os.environ.get("OCTOP_ALLOW_RUNTIME_PIP", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + +def local_embedding_deps_available() -> bool: + """True when fastembed can be imported (preferred local embedding runtime).""" + try: + import fastembed # noqa: F401 + except ImportError: + return False + return True + + +def _purge_failed_imports() -> None: + purge_import_cache(("fastembed", "huggingface_hub")) + + +def require_local_embedding_deps() -> None: + """Raise RuntimeError when optional local-embedding extras are missing.""" + if local_embedding_deps_available(): + return + raise RuntimeError( + "Local embedding components are not installed. " + "Enable the ONNX service to install them automatically." + ) + + +def ensure_local_embedding_deps(*, allow_install: bool | None = None) -> str: + """Ensure deps are importable. + + Admin ONNX actions pass ``allow_install=True``. Other callers default to + :func:`runtime_pip_allowed`. Returns ``\"ready\"`` or ``\"installed\"``. + """ + if local_embedding_deps_available(): + return "ready" + if allow_install is None: + allow_install = runtime_pip_allowed() + if not allow_install: + require_local_embedding_deps() + outcome = install_packages( + _LOCAL_EMBEDDING_SPEC, + is_satisfied=local_embedding_deps_available, + import_modules=("fastembed", "huggingface_hub"), + ) + if not local_embedding_deps_available(): + raise RuntimeError( + "Local embedding components were installed but could not be loaded. " + "Restart the server and try again." + ) + return outcome + + +async def ensure_local_embedding_deps_async( + *, + allow_install: bool | None = None, +) -> str: + """Async wrapper for :func:`ensure_local_embedding_deps`.""" + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, + lambda: ensure_local_embedding_deps(allow_install=allow_install), + ) + + +def normalize_onnx_model_id(model_name: str) -> str: + """Validate and normalize a model id; raises ValueError when unsafe.""" + name = model_name.strip() + if not name or len(name) > 200: + raise ValueError("invalid model id") + if ".." in name or "\\" in name or name.startswith("/") or name.endswith("/"): + raise ValueError("invalid model id") + if not _MODEL_ID_RE.fullmatch(name): + raise ValueError("invalid model id") + return name + + +def assert_catalog_model(model_name: str) -> str: + """Require *model_name* to be a known catalog entry.""" + name = normalize_onnx_model_id(model_name) + catalog_ids = {str(m["id"]) for m in list_onnx_catalog_models()} + if name not in catalog_ids: + raise ValueError(f"model not in ONNX catalog: {name}") + return name + + +class OnnxDownloadStatus(StrEnum): + IDLE = "idle" + DOWNLOADING = "downloading" + LOADING = "loading" + DONE = "done" + FAILED = "failed" + + +@dataclass +class OnnxDownloadState: + status: OnnxDownloadStatus = OnnxDownloadStatus.IDLE + progress: float = 0.0 + error: str | None = None + model_name: str = "" + task_id: str = "" + updated_at: float = field(default_factory=time.time) + + def to_dict(self) -> dict[str, Any]: + return { + "status": self.status.value, + "progress": self.progress, + "error": self.error, + "model_name": self.model_name, + "task_id": self.task_id, + } + + +@dataclass +class OnnxServiceConfig: + enabled: bool = False + model: str = _DEFAULT_MODEL + + def to_dict(self) -> dict[str, Any]: + return {"enabled": self.enabled, "model": self.model} + + @classmethod + def from_dict(cls, raw: dict[str, Any] | None) -> OnnxServiceConfig: + if not raw: + return cls() + model = str(raw.get("model") or _DEFAULT_MODEL).strip() or _DEFAULT_MODEL + return cls(enabled=bool(raw.get("enabled")), model=model) + + +def embedding_models_dir() -> Path: + path = PathLayout.from_env().root / "embedding_models" + path.mkdir(parents=True, exist_ok=True) + return path + + +def model_cache_dir(model_name: str) -> Path: + safe = "models--" + model_name.replace("/", "--") + return embedding_models_dir() / safe + + +def _downloaded_marker_path(model_name: str) -> Path: + return embedding_models_dir() / ".downloaded" / model_name.replace("/", "--") + + +def mark_model_downloaded(model_name: str) -> None: + path = _downloaded_marker_path(model_name) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(model_name, encoding="utf-8") + + +def _safe_under_cache(path: Path) -> Path | None: + """Return resolved path if it stays under embedding_models_dir(), else None.""" + root = embedding_models_dir().resolve() + try: + resolved = path.resolve() + except OSError: + return None + if resolved == root or root in resolved.parents: + return resolved + return None + + +def _cache_has_onnx_weights(path: Path) -> bool: + """True when *path* looks like a complete ONNX model cache (not a partial download).""" + if not path.is_dir(): + return False + safe = _safe_under_cache(path) + if safe is None: + return False + candidates = ( + "model.onnx", + "model_optimized.onnx", + "onnx/model.onnx", + "onnx/model_optimized.onnx", + ) + for rel in candidates: + if (safe / rel).is_file(): + return True + snapshots = safe / "snapshots" + if snapshots.is_dir(): + try: + for snap in snapshots.iterdir(): + if not snap.is_dir(): + continue + for rel in candidates: + if (snap / rel).is_file(): + return True + if any(snap.glob("*.onnx")): + return True + except OSError: + return False + try: + return any(safe.glob("*.onnx")) + except OSError: + return False + + +def _alias_cache_names(model_name: str) -> list[str]: + """HF / fastembed may store under Qdrant mirror ids, not the public model id.""" + names = [model_name] + meta = get_onnx_model_meta(model_name) + hf = meta.get("hf_source") + if isinstance(hf, str) and hf.strip() and hf not in names: + names.append(hf.strip()) + return names + + +def is_model_downloaded(model_name: str) -> bool: + """True only when catalog-safe id has actual ONNX weight files on disk.""" + try: + model_name = normalize_onnx_model_id(model_name) + except ValueError: + return False + for name in _alias_cache_names(model_name): + if _cache_has_onnx_weights(model_cache_dir(name)): + return True + suffix = name.split("/")[-1] if "/" in name else name + for candidate in ( + embedding_models_dir() / suffix, + embedding_models_dir() / f"fast-{suffix}", + ): + if _cache_has_onnx_weights(candidate): + return True + return False + + +def list_downloaded_models() -> list[str]: + root = embedding_models_dir() + found: set[str] = set() + catalog_ids = {str(m["id"]) for m in list_onnx_catalog_models()} + for mid in catalog_ids: + if is_model_downloaded(mid): + found.add(mid) + marker_root = root / ".downloaded" + if marker_root.is_dir(): + for child in marker_root.iterdir(): + if not child.is_file(): + continue + try: + mid = child.read_text(encoding="utf-8").strip() + except OSError: + mid = child.name.replace("--", "/", 1) + if mid in catalog_ids and is_model_downloaded(mid): + found.add(mid) + return sorted(found) + + +def delete_downloaded_model(model_name: str) -> bool: + name = assert_catalog_model(model_name) + removed = False + targets = [model_cache_dir(name), _downloaded_marker_path(name)] + for alias in _alias_cache_names(name): + targets.append(model_cache_dir(alias)) + suffix = alias.split("/")[-1] if "/" in alias else alias + targets.extend( + [ + embedding_models_dir() / suffix, + embedding_models_dir() / f"fast-{suffix}", + ] + ) + for path in targets: + safe = _safe_under_cache(path) + if safe is None: + continue + if safe.is_file(): + safe.unlink(missing_ok=True) + removed = True + elif safe.is_dir(): + shutil.rmtree(safe, ignore_errors=True) + removed = True + return removed + + +def _dir_size_bytes(path: Path) -> int: + total = 0 + if not path.exists(): + return 0 + for root, _dirs, files in os.walk(path): + for name in files: + try: + total += (Path(root) / name).stat().st_size + except OSError: + continue + return total + + +def load_config(settings_get: Any) -> OnnxServiceConfig: + raw = settings_get(_SETTINGS_KEY) + if not raw: + return OnnxServiceConfig() + try: + data = json.loads(raw) + except json.JSONDecodeError: + return OnnxServiceConfig() + if not isinstance(data, dict): + return OnnxServiceConfig() + return OnnxServiceConfig.from_dict(data) + + +def embedding_prerequisites_ok_for_model(model: str) -> bool: + if not local_embedding_deps_available(): + return False + if not model: + return False + try: + model = assert_catalog_model(model) + except ValueError: + return False + return is_model_downloaded(model) + + +def require_embedding_prerequisites_for_model(model: str) -> str: + require_local_embedding_deps() + if not model: + raise RuntimeError("ONNX embedding model is not configured or not downloaded") + model = assert_catalog_model(model) + if not is_model_downloaded(model): + raise RuntimeError("ONNX embedding model is not configured or not downloaded") + return model + + +def embedding_prerequisites_ok(settings_get: Any) -> bool: + cfg = load_config(settings_get) + return embedding_prerequisites_ok_for_model(cfg.model) + + +def require_embedding_prerequisites(settings_get: Any) -> OnnxServiceConfig: + cfg = load_config(settings_get) + require_embedding_prerequisites_for_model(cfg.model) + return cfg + + +def _build_text_embedding(model: str) -> Any: + from fastembed import TextEmbedding + + return TextEmbedding(model_name=model, cache_dir=str(embedding_models_dir())) + + +def embed_texts(model: str, texts: Sequence[str]) -> list[list[float]]: + if not texts: + return [] + require_local_embedding_deps() + model = assert_catalog_model(model) + emb = _build_text_embedding(model) + out = [[float(x) for x in vec] for vec in emb.embed(list(texts))] + if len(out) != len(texts): + raise RuntimeError("embedding count mismatch") + return out + + +def save_config(settings_set: Any, config: OnnxServiceConfig) -> OnnxServiceConfig: + settings_set(_SETTINGS_KEY, json.dumps(config.to_dict(), ensure_ascii=False)) + return config + + +def status_payload(settings_get: Any, download: OnnxDownloadState) -> dict[str, Any]: + config = load_config(settings_get) + downloaded = is_model_downloaded(config.model) if config.model else False + deps_ok = local_embedding_deps_available() + return { + "enabled": config.enabled, + "model": config.model, + "ready": bool(config.enabled and downloaded and deps_ok), + "downloaded": downloaded, + "cache_dir": str(embedding_models_dir()), + "download": download.to_dict(), + "local_models": list_downloaded_models(), + "presets": list(ONNX_PRESET_MODEL_IDS), + "deps_available": deps_ok, + } + + +class OnnxDownloadManager: + """In-process download task tracker (one active download at a time).""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self._state = OnnxDownloadState() + self._task: asyncio.Task[None] | None = None + + @property + def state(self) -> OnnxDownloadState: + with self._lock: + return OnnxDownloadState( + status=self._state.status, + progress=self._state.progress, + error=self._state.error, + model_name=self._state.model_name, + task_id=self._state.task_id, + updated_at=self._state.updated_at, + ) + + def _set(self, **kwargs: Any) -> None: + with self._lock: + for key, value in kwargs.items(): + setattr(self._state, key, value) + self._state.updated_at = time.time() + + async def start_download(self, model_name: str) -> OnnxDownloadState: + model_name = assert_catalog_model(model_name) + await ensure_local_embedding_deps_async(allow_install=True) + if is_model_downloaded(model_name): + self._set( + status=OnnxDownloadStatus.DONE, + progress=1.0, + error=None, + model_name=model_name, + task_id="", + ) + return self.state + + task_id = uuid.uuid4().hex + with self._lock: + if self._state.status in { + OnnxDownloadStatus.DOWNLOADING, + OnnxDownloadStatus.LOADING, + }: + raise RuntimeError("another ONNX download is already running") + self._state.status = OnnxDownloadStatus.DOWNLOADING + self._state.progress = 0.05 + self._state.error = None + self._state.model_name = model_name + self._state.task_id = task_id + self._state.updated_at = time.time() + + loop = asyncio.get_running_loop() + + async def _run() -> None: + try: + await loop.run_in_executor(None, self._download_sync, model_name) + self._set( + status=OnnxDownloadStatus.DONE, + progress=1.0, + error=None, + model_name=model_name, + ) + except Exception as exc: + logger.exception("ONNX model download failed: %s", model_name) + self._set( + status=OnnxDownloadStatus.FAILED, + progress=0.0, + error=str(exc), + model_name=model_name, + ) + + self._task = asyncio.create_task(_run()) + return self.state + + def _download_sync(self, model_name: str) -> None: + cache = embedding_models_dir() + meta = get_onnx_model_meta(model_name) + size_gb = meta.get("size_gb") + expected_bytes: int | None = None + try: + if size_gb is not None: + expected_bytes = int(float(size_gb) * (1024**3)) + except (TypeError, ValueError): + expected_bytes = None + + stop = threading.Event() + baseline = _dir_size_bytes(cache) + + def _watch() -> None: + while not stop.wait(0.5): + grown = max(0, _dir_size_bytes(cache) - baseline) + if expected_bytes and expected_bytes > 0: + ratio = min(0.92, 0.08 + 0.84 * (grown / expected_bytes)) + else: + # Unknown size: creep toward 0.85 while bytes keep growing. + ratio = min(0.85, 0.08 + (grown / (200 * 1024 * 1024)) * 0.7) + self._set(status=OnnxDownloadStatus.DOWNLOADING, progress=ratio) + + watcher = threading.Thread(target=_watch, name="onnx-dl-progress", daemon=True) + watcher.start() + self._set(status=OnnxDownloadStatus.DOWNLOADING, progress=0.08) + try: + try: + from fastembed import TextEmbedding + + self._set( + status=OnnxDownloadStatus.LOADING, + progress=max(0.15, self.state.progress), + ) + TextEmbedding(model_name=model_name, cache_dir=str(cache)) + mark_model_downloaded(model_name) + self._set(progress=0.95) + return + except ImportError: + pass + + try: + from huggingface_hub import snapshot_download + except ImportError as exc: + raise RuntimeError( + "Local embedding download requires optional components that " + "could not be loaded. Enable the ONNX service to install them " + "automatically." + ) from exc + + repo_id = str(meta.get("hf_source") or model_name) + self._set( + status=OnnxDownloadStatus.DOWNLOADING, + progress=max(0.12, self.state.progress), + ) + snapshot_download(repo_id=repo_id, cache_dir=str(cache)) + mark_model_downloaded(model_name) + self._set(progress=0.95) + finally: + stop.set() + watcher.join(timeout=2.0) + + +DOWNLOAD_MANAGER = OnnxDownloadManager() diff --git a/src/octop/infra/agents/providers/presets.py b/src/octop/infra/agents/providers/presets.py index b83f0b82..c707662c 100644 --- a/src/octop/infra/agents/providers/presets.py +++ b/src/octop/infra/agents/providers/presets.py @@ -230,6 +230,33 @@ def load_provider_presets() -> list[dict[str, Any]]: "logo_id": "openai", }, ) + if not any(p.get("id") == "onnx" for p in out): + from octop.infra.agents.providers.onnx_catalog import ONNX_PRESET_MODEL_IDS + + onnx_preset = { + "id": "onnx", + "name": "ONNX (Local)", + "base_url": "", + "protocol": "openai", + "api_key_prefix": "", + "models": [ + { + "id": mid, + "name": mid, + "enabled": False, + "embedding": True, + "task": "embedding", + "input": ["text"], + } + for mid in ONNX_PRESET_MODEL_IDS + ], + "logo_id": "onnx", + } + insert_at = next( + (i + 1 for i, p in enumerate(out) if p.get("id") == "ollama"), + len(out), + ) + out.insert(insert_at, onnx_preset) for preset in out: provider_id = str(preset.get("id") or "") for model in preset.get("models") or []: diff --git a/src/octop/infra/agents/providers/probe.py b/src/octop/infra/agents/providers/probe.py index b47b70d4..2f5f036a 100644 --- a/src/octop/infra/agents/providers/probe.py +++ b/src/octop/infra/agents/providers/probe.py @@ -17,6 +17,7 @@ _DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1" _FETCH_MODELS_TIMEOUT_S = 30.0 +_EMBEDDING_PROBE_TEXT = "ping" def provider_headers(row: Any) -> dict[str, str]: @@ -83,23 +84,113 @@ def make_probe_provider_row( base_url: str | None, model_id: str, extra_json: str | None = None, + embedding: bool = False, ) -> Any: """Build a ProviderRow-like object for connectivity probes.""" + model: dict[str, Any] = {"id": model_id, "name": model_id} + if embedding: + model["embedding"] = True + model["task"] = "embedding" return SimpleNamespace( name=name, kind=kind, base_url=base_url, api_key=api_key, extra_json=extra_json, - get_models=lambda: [{"id": model_id, "name": model_id}], + get_models=lambda: [model], ) -async def probe_provider_row(row: Any, *, model_id: str | None = None) -> dict[str, Any]: - """Probe a provider by sending a one-token ping and timing it.""" +def _probe_model_id(row: Any, model_id: str | None) -> str: + models = row.get_models() if hasattr(row, "get_models") else [] + if model_id: + return model_id + if models: + return str(models[0].get("id") or "gpt-4o-mini") + return "gpt-4o-mini" + + +def _probe_model_entry(row: Any, model_id: str) -> dict[str, Any]: + models = row.get_models() if hasattr(row, "get_models") else [] + entry = next((m for m in models if isinstance(m, dict) and m.get("id") == model_id), None) + return entry if isinstance(entry, dict) else {} + + +def _should_probe_embedding(row: Any, *, model_id: str, embedding: bool | None) -> bool: + if embedding is True: + return True + from octop.infra.agents.providers.model_flags import is_embedding_model + + return is_embedding_model( + _probe_model_entry(row, model_id), + provider_name=getattr(row, "name", None), + provider_api_key=getattr(row, "api_key", None), + ) + + +def _embeddings_url(base_url: str | None) -> str: + root = (base_url or "").strip().rstrip("/") or _DEFAULT_OPENAI_BASE_URL + return f"{root}/embeddings" + + +async def _probe_embedding_endpoint(row: Any, *, model_id: str) -> dict[str, Any]: + """POST OpenAI-compatible ``{base}/embeddings`` and time the round-trip.""" + started = time.perf_counter() + url = _embeddings_url(getattr(row, "base_url", None)) + headers: dict[str, str] = {"Authorization": f"Bearer {getattr(row, 'api_key', None) or ''}"} + extra = provider_headers(row) + if extra: + headers.update(extra) + try: + async with httpx.AsyncClient(timeout=_FETCH_MODELS_TIMEOUT_S) as client: + response = await client.post( + url, + headers=headers, + json={"model": model_id, "input": [_EMBEDDING_PROBE_TEXT]}, + ) + except Exception as exc: + logger.info("embedding probe failed for %s: %s", getattr(row, "name", "?"), exc) + return {"ok": False, "error": str(exc)} + + if response.status_code >= 400: + detail = response.text.strip() + if len(detail) > 300: + detail = detail[:300] + "…" + error = f"HTTP {response.status_code} POST {url}" + if detail: + error = f"{error}: {detail}" + return {"ok": False, "error": error} + + try: + payload = response.json() + except Exception: + return { + "ok": False, + "error": "response is not valid JSON (expected OpenAI-compatible /embeddings)", + } + + data = payload.get("data") if isinstance(payload, dict) else None + first = data[0] if isinstance(data, list) and data else None + vector = first.get("embedding") if isinstance(first, dict) else None + if not isinstance(vector, list) or not vector: + return { + "ok": False, + "error": "response is not OpenAI-compatible (expected {data: [{embedding, …}]})", + } + latency_ms = int((time.perf_counter() - started) * 1000) + return {"ok": True, "latency_ms": latency_ms} + + +async def probe_provider_row( + row: Any, *, model_id: str | None = None, embedding: bool | None = None +) -> dict[str, Any]: + """Probe a provider: chat models get a one-token ping; embedding models POST /embeddings.""" + mid = _probe_model_id(row, model_id) + if _should_probe_embedding(row, model_id=mid, embedding=embedding): + return await _probe_embedding_endpoint(row, model_id=mid) started = time.perf_counter() try: - chat = build_probe_chat_model(row, model_id=model_id) + chat = build_probe_chat_model(row, model_id=mid) result = await asyncio.wait_for(chat.ainvoke("ping"), timeout=30.0) except Exception as exc: logger.info("provider probe failed for %s: %s", getattr(row, "name", "?"), exc) diff --git a/src/octop/infra/agents/providers/resolved.py b/src/octop/infra/agents/providers/resolved.py index dea395f1..c2f6e7b3 100644 --- a/src/octop/infra/agents/providers/resolved.py +++ b/src/octop/infra/agents/providers/resolved.py @@ -4,17 +4,26 @@ from typing import Any +from octop.infra.agents.providers.model_flags import is_chat_eligible_model from octop.infra.agents.providers.reasoning import reasoning_capability def list_resolved_models(providers: list[Any]) -> list[dict[str, Any]]: - """Return enabled models for providers that have credentials configured.""" + """Return chat-eligible enabled models for providers with credentials. + + Embedding-only models (``embedding: true`` / ``task: embedding`` / ONNX + local) are excluded so they never appear in chat pickers or auto-route. + """ resolved: list[dict[str, Any]] = [] for provider in providers: if not provider.enabled or not provider.api_key: continue + provider_name = str(getattr(provider, "name", "") or "") + provider_api_key = str(getattr(provider, "api_key", "") or "") or None for m in provider.get_models(): - if not m.get("enabled", True): + if not is_chat_eligible_model( + m, provider_name=provider_name, provider_api_key=provider_api_key + ): continue # Catalogs may store either name; both feed the chat context ring. window = m.get("max_input_tokens") or m.get("context_window") diff --git a/src/octop/infra/agents/providers/store.py b/src/octop/infra/agents/providers/store.py index ea2c5d06..f09a3217 100644 --- a/src/octop/infra/agents/providers/store.py +++ b/src/octop/infra/agents/providers/store.py @@ -8,6 +8,7 @@ from harness_agent.config import ModelConfig, ProviderConfig +from octop.infra.agents.providers.model_flags import is_chat_eligible_model from octop.infra.agents.providers.reasoning import reasoning_capability if TYPE_CHECKING: @@ -53,13 +54,20 @@ def _model_dict_supports_image(model: dict[str, Any]) -> bool: return "image" in _infer_model_input_modalities(model_id, explicit) -def enabled_model_refs(provider_name: str, models: list[dict[str, Any]]) -> set[str]: - """Return ``provider/model`` refs for enabled entries with a non-empty id.""" +def enabled_model_refs( + provider_name: str, + models: list[dict[str, Any]], + *, + provider_api_key: str | None = None, +) -> set[str]: + """Return ``provider/model`` refs for chat-eligible enabled entries.""" refs: set[str] = set() for model in models: - model_id = str(model.get("id") or "").strip() - if not model_id or not model.get("enabled", True): + if not is_chat_eligible_model( + model, provider_name=provider_name, provider_api_key=provider_api_key + ): continue + model_id = str(model.get("id") or "").strip() refs.add(f"{provider_name}/{model_id}") return refs @@ -112,7 +120,10 @@ def iter_usable_rows(self) -> Iterator[Any]: models = row.get_models() if not models: continue - if any(m.get("enabled", True) for m in models): + if any( + is_chat_eligible_model(m, provider_name=row.name, provider_api_key=row.api_key) + for m in models + ): yield row def has_usable_providers(self) -> bool: @@ -124,7 +135,13 @@ def build_harness_configs(self) -> list[ProviderConfig]: for row in self.iter_usable_rows(): protocol = KIND_TO_PROTOCOL.get(row.kind, "openai") raw_models = json.loads(row.models_json) if getattr(row, "models_json", None) else [] - models = [self._model_config_from_row(m) for m in raw_models] + models = [ + self._model_config_from_row(m) + for m in raw_models + if is_chat_eligible_model(m, provider_name=row.name, provider_api_key=row.api_key) + ] + if not models: + continue headers: dict[str, str] = {} if row.extra_json: try: @@ -169,7 +186,9 @@ def is_model_ref_multimodal(self, ref: str) -> bool: if row is None: return False for model in row.get_models(): - if model.get("id") == model_id and model.get("enabled", True): + if model.get("id") == model_id and is_chat_eligible_model( + model, provider_name=provider_name, provider_api_key=row.api_key + ): return _model_dict_supports_image(model) return False @@ -177,7 +196,9 @@ def resolve_multimodal_model_ref(self) -> str | None: """First enabled image-capable model across usable providers.""" for row in self.iter_usable_rows(): for model in row.get_models(): - if not model.get("enabled", True): + if not is_chat_eligible_model( + model, provider_name=row.name, provider_api_key=row.api_key + ): continue if _model_dict_supports_image(model): return f"{row.name}/{model['id']}" @@ -199,10 +220,12 @@ def resolve_model_for_multimodal_turn( return upgraded or ref def resolve_first_model_ref(self) -> str | None: - """First enabled model across usable providers, for lightweight internal tasks.""" + """First chat-eligible enabled model across usable providers.""" for row in self.iter_usable_rows(): for model in row.get_models(): - if not model.get("enabled", True): + if not is_chat_eligible_model( + model, provider_name=row.name, provider_api_key=row.api_key + ): continue model_id = str(model.get("id") or "").strip() if model_id: @@ -210,7 +233,7 @@ def resolve_first_model_ref(self) -> str | None: return None def is_model_ref_usable(self, ref: str) -> bool: - """Return True when *ref* points at an enabled model on a usable provider row.""" + """Return True when *ref* points at a chat-eligible model on a usable provider.""" ref = ref.strip() if not ref or ref.lower() == "auto" or "/" not in ref: return False @@ -221,8 +244,11 @@ def is_model_ref_usable(self, ref: str) -> bool: if row is None or not row.enabled or not row.api_key or not row.base_url: return False for model in row.get_models(): - if model.get("id") == model_id and model.get("enabled", True): - return True + if model.get("id") != model_id: + continue + return is_chat_eligible_model( + model, provider_name=provider_name, provider_api_key=row.api_key + ) return False def get_model_reasoning_capability(self, ref: str) -> dict[str, Any] | None: diff --git a/src/octop/infra/agents/thread_fork.py b/src/octop/infra/agents/thread_fork.py new file mode 100644 index 00000000..6b5fe2b6 --- /dev/null +++ b/src/octop/infra/agents/thread_fork.py @@ -0,0 +1,219 @@ +"""Fork a conversation thread from a selected user message. + +Copies LangGraph checkpoint messages *strictly before* that user turn into a +new thread, leaving the source transcript unchanged. The selected question is +intentionally omitted so the dashboard can prefill the composer and send a +different follow-up — the non-destructive counterpart of "edit message". +""" + +from __future__ import annotations + +import logging +from typing import Any + +from octop.i18n import tr +from octop.infra.db.repos.threads import ThreadRow +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.gateway.threads import ThreadRegistry + +logger = logging.getLogger(__name__) + +# Full-transcript read for fork; history HTTP pagination uses a much smaller cap. +_FORK_HISTORY_LIMIT = 100_000 + + +def _fork_title(source_title: str | None, locale: str) -> str: + """Return a sidebar title that distinguishes a fork from its source thread.""" + base = (source_title or "").strip() + if base: + return f"{base}{tr('threads.fork_title_suffix', locale)}" + return tr("threads.fork_default_title", locale) + + +def _msg_id(msg: Any) -> str: + raw = msg.get("id") if isinstance(msg, dict) else getattr(msg, "id", None) + return str(raw).strip() if raw else "" + + +def _is_user_message(msg: Any) -> bool: + if isinstance(msg, dict): + role = str(msg.get("role") or "") + if role: + return role == "user" + msg_type = str(msg.get("type") or "") + return msg_type in ("human", "user") + type_name = type(msg).__name__ + if "HumanMessage" in type_name: + return True + role = str(getattr(msg, "role", None) or getattr(msg, "type", "") or "") + return role in ("user", "human") + + +def _user_text(msg: Any) -> str: + content = msg.get("content") if isinstance(msg, dict) else getattr(msg, "content", "") + if isinstance(content, str): + return content.strip() + if isinstance(content, list): + parts: list[str] = [] + for block in content: + if isinstance(block, str): + if block.strip(): + parts.append(block.strip()) + elif isinstance(block, dict) and str(block.get("type") or "") == "text": + text = str(block.get("text") or "").strip() + if text: + parts.append(text) + return "\n".join(parts).strip() + return str(content or "").strip() + + +def find_user_fork_index( + messages: list[Any], + *, + message_id: str, + content: str | None = None, + user_turns_from_end: int | None = None, +) -> int: + """Return the index of the selected user message in *messages*. + + Prefers a suffix count of user turns (stable across client-generated UI + ids), then checkpoint ``id``, then exact text content. + """ + humans = [i for i, msg in enumerate(messages) if _is_user_message(msg)] + if not humans: + raise OctopError(ErrorCode.NOT_FOUND, "no user message to fork from") + + wanted_id = (message_id or "").strip() + wanted_text = (content or "").strip() + + if ( + user_turns_from_end is not None + and user_turns_from_end >= 1 + and user_turns_from_end <= len(humans) + ): + idx = humans[-user_turns_from_end] + if not wanted_text or _user_text(messages[idx]) == wanted_text: + return idx + if wanted_id and _msg_id(messages[idx]) == wanted_id: + return idx + + if wanted_id: + for idx in humans: + if _msg_id(messages[idx]) == wanted_id: + return idx + + if wanted_text: + matches = [idx for idx in humans if _user_text(messages[idx]) == wanted_text] + if matches: + return matches[-1] + + raise OctopError(ErrorCode.NOT_FOUND, "user message not found in thread") + + +async def load_checkpoint_messages(harness: Any, thread_id: str) -> list[Any]: + """Return the full checkpoint transcript (oldest first).""" + graph = getattr(harness, "graph", None) + if graph is not None and hasattr(graph, "aget_state"): + try: + state = await graph.aget_state({"configurable": {"thread_id": thread_id}}) + raw = list((state.values or {}).get("messages") or []) if state is not None else [] + if raw: + return raw + except Exception: + logger.warning( + "fork: graph.aget_state failed for thread=%s", + thread_id, + exc_info=True, + ) + aget_history = getattr(harness, "aget_history", None) + if aget_history is not None: + return list(await aget_history(thread_id, limit=_FORK_HISTORY_LIMIT)) + return [] + + +async def write_checkpoint_messages( + harness: Any, + *, + thread_id: str, + messages: list[Any], +) -> None: + """Seed *thread_id* with *messages* (no-op when the list is empty).""" + if not messages: + return + graph = getattr(harness, "graph", None) + aupdate = getattr(graph, "aupdate_state", None) if graph is not None else None + if aupdate is None: + raise OctopError( + ErrorCode.AGENT_NOT_RUNNING, + "agent checkpointer cannot copy thread state", + ) + await aupdate( + {"configurable": {"thread_id": thread_id}}, + {"messages": messages}, + ) + + +async def fork_dashboard_thread( + *, + thread_registry: ThreadRegistry, + harness: Any, + source: ThreadRow, + user_id: int, + message_id: str, + content: str | None = None, + user_turns_from_end: int | None = None, + locale: str = "en", +) -> dict[str, Any]: + """Create a dashboard thread seeded with the source prefix before *message_id*.""" + wanted_id = (message_id or "").strip() + if not wanted_id: + raise OctopError(ErrorCode.SLASH_BAD_ARGS, "message_id is required") + + messages = await load_checkpoint_messages(harness, source.thread_id) + idx = find_user_fork_index( + messages, + message_id=wanted_id, + content=content, + user_turns_from_end=user_turns_from_end, + ) + prefix = messages[:idx] + + session_key = ThreadRegistry.dashboard_key(agent_id=source.agent_id, user_id=user_id) + dest_id = thread_registry.create_thread( + agent_id=source.agent_id, + user_id=user_id, + channel_type=ThreadRegistry.CHANNEL_DASHBOARD, + session_key=session_key, + title=None, + last_active=0, + ) + try: + await write_checkpoint_messages(harness, thread_id=dest_id, messages=prefix) + except Exception: + thread_registry.delete_thread(dest_id) + raise + + copied = len(prefix) + if copied > 0: + thread_registry.update_title(dest_id, _fork_title(source.title, locale)) + thread_registry.update_composer( + dest_id, + model_ref=source.model_ref, + reasoning_mode=source.reasoning_mode, + reasoning_effort=source.reasoning_effort, + ) + await thread_registry.rebind( + session_key=session_key, + thread_id=dest_id, + agent_id=source.agent_id, + ) + row = thread_registry.get_thread(dest_id) + return { + "thread_id": dest_id, + "session_key": session_key, + "source_thread_id": source.thread_id, + "copied_messages": copied, + "title": row.title if row is not None else None, + "last_active": row.last_active if row is not None else 0, + "created_at": row.created_at if row is not None else 0, + } diff --git a/src/octop/infra/auth/__init__.py b/src/octop/infra/auth/__init__.py new file mode 100644 index 00000000..edf7c8b8 --- /dev/null +++ b/src/octop/infra/auth/__init__.py @@ -0,0 +1 @@ +"""Authentication domain helpers.""" diff --git a/src/octop/infra/auth/sso/__init__.py b/src/octop/infra/auth/sso/__init__.py new file mode 100644 index 00000000..f5e15f3f --- /dev/null +++ b/src/octop/infra/auth/sso/__init__.py @@ -0,0 +1,5 @@ +"""OpenID Connect single sign-on helpers.""" + +from octop.infra.auth.sso.service import SsoService + +__all__ = ["SsoService"] diff --git a/src/octop/infra/auth/sso/crypto.py b/src/octop/infra/auth/sso/crypto.py new file mode 100644 index 00000000..52a43e82 --- /dev/null +++ b/src/octop/infra/auth/sso/crypto.py @@ -0,0 +1,24 @@ +"""Fernet encryption for OIDC single sign-on secrets.""" + +from __future__ import annotations + +from cryptography.fernet import Fernet + +from octop.infra.db.repos.secrets import SecretRepo + +_FERNET_KEY = "sso_fernet" + + +def _get_fernet(secret_repo: SecretRepo) -> Fernet: + raw = secret_repo.get_or_create(_FERNET_KEY, Fernet.generate_key) + return Fernet(raw) + + +def encrypt_secret(secret_repo: SecretRepo, plain: str) -> bytes: + """Encrypt an SSO secret using the installation-specific Fernet key.""" + return _get_fernet(secret_repo).encrypt(plain.encode("utf-8")) + + +def decrypt_secret(secret_repo: SecretRepo, encrypted: bytes) -> str: + """Decrypt an SSO secret using the installation-specific Fernet key.""" + return _get_fernet(secret_repo).decrypt(encrypted).decode("utf-8") diff --git a/src/octop/infra/auth/sso/discovery.py b/src/octop/infra/auth/sso/discovery.py new file mode 100644 index 00000000..b03abb42 --- /dev/null +++ b/src/octop/infra/auth/sso/discovery.py @@ -0,0 +1,48 @@ +"""OpenID Connect discovery document helpers.""" + +from __future__ import annotations + +import time +from typing import Any, cast + +import httpx + +_DISCOVERY_PATH = "/.well-known/openid-configuration" +_CACHE_TTL_SECONDS = 3600 + + +def _normalized_issuer(issuer: str) -> str: + return issuer.rstrip("/") + + +def fetch_discovery(issuer: str, *, httpx_client: httpx.Client) -> dict[str, Any]: + """Fetch an issuer's OpenID Connect discovery document.""" + normalized = _normalized_issuer(issuer) + response = httpx_client.get(f"{normalized}{_DISCOVERY_PATH}") + response.raise_for_status() + discovery = cast(dict[str, Any], response.json()) + doc_issuer = discovery.get("issuer") + if not isinstance(doc_issuer, str) or _normalized_issuer(doc_issuer) != normalized: + raise ValueError("OIDC discovery issuer does not match configured issuer") + return discovery + + +class DiscoveryCache: + """In-process cache for OpenID Connect discovery documents.""" + + def __init__(self) -> None: + self._entries: dict[str, tuple[float, dict[str, Any]]] = {} + + def get(self, issuer: str, *, httpx_client: httpx.Client) -> dict[str, Any]: + normalized_issuer = _normalized_issuer(issuer) + entry = self._entries.get(normalized_issuer) + now = time.monotonic() + if entry is not None and entry[0] > now: + return entry[1] + + discovery = fetch_discovery(normalized_issuer, httpx_client=httpx_client) + self._entries[normalized_issuer] = (now + _CACHE_TTL_SECONDS, discovery) + return discovery + + def invalidate(self, issuer: str) -> None: + self._entries.pop(_normalized_issuer(issuer), None) diff --git a/src/octop/infra/auth/sso/id_token.py b/src/octop/infra/auth/sso/id_token.py new file mode 100644 index 00000000..fdbe6144 --- /dev/null +++ b/src/octop/infra/auth/sso/id_token.py @@ -0,0 +1,58 @@ +"""OpenID Connect ID token verification.""" + +from __future__ import annotations + +from typing import Any + +import httpx as httpx_module +import jwt + +# Only asymmetric algorithms used by mainstream OIDC IdPs. +_ALLOWED_ALGORITHMS = ("RS256", "ES256") + + +def _signing_key(id_token: str, *, jwks_uri: str, httpx: httpx_module.Client) -> jwt.PyJWK: + header = jwt.get_unverified_header(id_token) + alg = header.get("alg") + if alg not in _ALLOWED_ALGORITHMS: + raise jwt.InvalidTokenError(f"ID token algorithm {alg!r} is not allowed") + key_id = header.get("kid") + if not isinstance(key_id, str): + raise jwt.InvalidTokenError("ID token is missing a key ID") + + response = httpx.get(jwks_uri) + response.raise_for_status() + jwks = response.json() + keys = jwks.get("keys") if isinstance(jwks, dict) else None + if not isinstance(keys, list): + raise jwt.InvalidTokenError("JWKS response has no keys") + + for key_data in keys: + if isinstance(key_data, dict) and key_data.get("kid") == key_id: + return jwt.PyJWK.from_dict(key_data) + raise jwt.InvalidTokenError("ID token signing key was not found") + + +def verify_id_token( + id_token: str, + *, + jwks_uri: str, + issuer: str, + client_id: str, + nonce: str, + httpx: httpx_module.Client, +) -> dict[str, Any]: + """Verify an OIDC ID token and return its claims.""" + signing_key = _signing_key(id_token, jwks_uri=jwks_uri, httpx=httpx) + claims = jwt.decode( + id_token, + signing_key.key, + algorithms=list(_ALLOWED_ALGORITHMS), + audience=client_id, + issuer=issuer, + leeway=60, + options={"require": ["exp", "iat"]}, + ) + if claims.get("nonce") != nonce: + raise jwt.InvalidTokenError("ID token nonce does not match") + return claims diff --git a/src/octop/infra/auth/sso/pkce.py b/src/octop/infra/auth/sso/pkce.py new file mode 100644 index 00000000..2dcdaf5e --- /dev/null +++ b/src/octop/infra/auth/sso/pkce.py @@ -0,0 +1,14 @@ +"""PKCE helpers for OIDC authorization flows.""" + +from __future__ import annotations + +import base64 +import hashlib +import secrets + + +def new_pkce_pair() -> tuple[str, str]: + verifier = secrets.token_urlsafe(64) + digest = hashlib.sha256(verifier.encode("ascii")).digest() + challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + return verifier, challenge diff --git a/src/octop/infra/auth/sso/public_base.py b/src/octop/infra/auth/sso/public_base.py new file mode 100644 index 00000000..f2f63799 --- /dev/null +++ b/src/octop/infra/auth/sso/public_base.py @@ -0,0 +1,10 @@ +"""Public URL helpers for OIDC callbacks (framework-free).""" + +from __future__ import annotations + +_OIDC_CALLBACK_PATH = "/api/auth/oidc/callback" + + +def build_redirect_uri(public_base: str) -> str: + """Build Octop's OIDC callback URL from a public origin.""" + return f"{public_base.rstrip('/')}{_OIDC_CALLBACK_PATH}" diff --git a/src/octop/infra/auth/sso/redirect_after.py b/src/octop/infra/auth/sso/redirect_after.py new file mode 100644 index 00000000..39c54abf --- /dev/null +++ b/src/octop/infra/auth/sso/redirect_after.py @@ -0,0 +1,14 @@ +"""Safe post-login redirect path handling.""" + +from __future__ import annotations + +_DEFAULT_REDIRECT_AFTER = "/chat" + + +def sanitize_redirect_after(path: str | None) -> str: + """Return a safe internal redirect path, or the default chat route.""" + if not path or not path.startswith("/"): + return _DEFAULT_REDIRECT_AFTER + if path.startswith("//") or "\\" in path or "://" in path or path.startswith("http:"): + return _DEFAULT_REDIRECT_AFTER + return path diff --git a/src/octop/infra/auth/sso/service.py b/src/octop/infra/auth/sso/service.py new file mode 100644 index 00000000..ff1f3640 --- /dev/null +++ b/src/octop/infra/auth/sso/service.py @@ -0,0 +1,368 @@ +"""OpenID Connect SSO service orchestration.""" + +from __future__ import annotations + +import asyncio +import secrets +import time +from collections.abc import Mapping +from dataclasses import dataclass +from functools import partial +from typing import Any +from urllib.parse import urlencode + +import httpx +import jwt + +from octop.infra.auth.sso.crypto import decrypt_secret, encrypt_secret +from octop.infra.auth.sso.discovery import DiscoveryCache +from octop.infra.auth.sso.id_token import verify_id_token +from octop.infra.auth.sso.pkce import new_pkce_pair +from octop.infra.auth.sso.public_base import build_redirect_uri +from octop.infra.auth.sso.redirect_after import sanitize_redirect_after +from octop.infra.db.repos.secrets import SecretRepo +from octop.infra.db.repos.sso import SsoProviderRow +from octop.infra.db.services import SharedServices +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.identity import User +from octop.infra.users.manager import UserManager + +_LOGIN_STATE_TTL_SECONDS = 600 +_LOGIN_CODE_TTL_SECONDS = 60 +_HTTP_TIMEOUT = httpx.Timeout(10.0, connect=5.0) + + +@dataclass(frozen=True) +class RedirectResult: + """A browser redirect produced by an OIDC callback.""" + + url: str + + +class SsoService: + """Coordinates OIDC configuration and browser login flows.""" + + def __init__(self, services: SharedServices, user_manager: UserManager) -> None: + self._services = services + self._user_manager = user_manager + self._discovery = DiscoveryCache() + + def status(self) -> dict[str, bool | str]: + provider = self._services.sso_repo.get_provider() + enabled = bool( + provider + and provider.enabled + and provider.issuer.strip() + and self._user_manager.count() > 0 + ) + return { + "enabled": enabled, + "display_name": provider.display_name if provider is not None else "", + } + + def get_config_for_admin(self, *, public_base: str) -> dict[str, Any]: + provider = self._services.sso_repo.get_provider() + if provider is None: + return { + "enabled": False, + "display_name": "", + "issuer": "", + "client_id": "", + "scopes": "openid profile email", + "dashboard_origin": None, + "has_client_secret": False, + "redirect_uri": build_redirect_uri(public_base), + } + return { + "enabled": bool(provider.enabled), + "display_name": provider.display_name, + "issuer": provider.issuer, + "client_id": provider.client_id, + "scopes": provider.scopes, + "dashboard_origin": provider.dashboard_origin, + "has_client_secret": provider.client_secret_enc is not None, + "redirect_uri": build_redirect_uri(public_base), + } + + def put_config( + self, + body: Mapping[str, object], + *, + secret_repo: SecretRepo | None = None, + public_base: str | None = None, + ) -> dict[str, Any]: + current = self._services.sso_repo.get_provider() + secret = body.get("client_secret") + encrypted_secret: bytes | None = None + if isinstance(secret, str) and secret: + encrypted_secret = encrypt_secret(secret_repo or self._services.secret_repo, secret) + + provider = self._services.sso_repo.upsert_provider( + enabled=bool(body.get("enabled", current.enabled if current else False)), + display_name=self._string(body, "display_name", current, "Octop SSO"), + issuer=self._string(body, "issuer", current, ""), + client_id=self._string(body, "client_id", current, ""), + client_secret_enc=encrypted_secret, + scopes=self._string(body, "scopes", current, "openid profile email"), + dashboard_origin=self._nullable_string(body, "dashboard_origin", current), + ) + self._discovery.invalidate(provider.issuer) + if public_base is not None: + return self.get_config_for_admin(public_base=public_base) + return self._provider_config(provider) + + def test_connection(self) -> dict[str, bool | str]: + provider = self._configured_provider() + self._discovery.invalidate(provider.issuer) + try: + with self._http_client() as client: + discovery = self._discovery.get(provider.issuer, httpx_client=client) + jwks_uri = self._endpoint(discovery, "jwks_uri") + response = client.get(jwks_uri) + response.raise_for_status() + jwks = response.json() + if not isinstance(jwks, dict) or not isinstance(jwks.get("keys"), list): + raise ValueError("JWKS response has no keys") + except (httpx.HTTPError, ValueError) as exc: + return {"ok": False, "detail": str(exc)} + return {"ok": True, "detail": "OIDC discovery and JWKS are reachable"} + + def start_login(self, *, redirect_after: str | None, public_base: str) -> dict[str, str]: + self._services.sso_repo.delete_expired() + provider = self._enabled_provider() + nonce = secrets.token_urlsafe(32) + state = secrets.token_urlsafe(32) + verifier, challenge = new_pkce_pair() + redirect_uri = build_redirect_uri(public_base) + with self._http_client() as client: + discovery = self._discovery.get(provider.issuer, httpx_client=client) + authorization_endpoint = self._endpoint(discovery, "authorization_endpoint") + self._services.sso_repo.create_login_state( + state=state, + provider_id=provider.id, + nonce=nonce, + code_verifier=verifier, + redirect_after=sanitize_redirect_after(redirect_after), + expires_at=int(time.time()) + _LOGIN_STATE_TTL_SECONDS, + ) + query = urlencode( + { + "response_type": "code", + "client_id": provider.client_id, + "redirect_uri": redirect_uri, + "scope": provider.scopes, + "state": state, + "nonce": nonce, + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + return { + "authorization_url": f"{authorization_endpoint}?{query}", + "state": state, + } + + async def handle_callback( + self, + *, + code: str | None, + state: str | None, + error: str | None, + public_base: str, + ) -> RedirectResult: + self._services.sso_repo.delete_expired() + frontend = self._frontend_base(public_base) + if error is not None: + return self._error_redirect( + frontend, "denied" if error == "access_denied" else "generic" + ) + if not state: + return self._error_redirect(frontend, "state") + + login_state = self._services.sso_repo.take_login_state(state) + if login_state is None: + return self._error_redirect(frontend, "state") + provider = self._services.sso_repo.get_provider() + if provider is None or provider.id != login_state.provider_id or not provider.enabled: + return self._error_redirect(frontend, "disabled") + if not provider.issuer.strip() or not provider.client_id.strip() or not code: + return self._error_redirect(frontend, "misconfigured") + + # Prefer configured dashboard origin once provider is known. + frontend = self._frontend_base(public_base, provider=provider) + + try: + claims = await asyncio.get_running_loop().run_in_executor( + None, + partial( + self._exchange_claims, + provider, + code, + login_state.code_verifier, + login_state.nonce, + public_base, + ), + ) + except jwt.InvalidTokenError: + return self._error_redirect(frontend, "invalid_token") + except (httpx.HTTPError, ValueError): + return self._error_redirect(frontend, "exchange") + + subject = claims.get("sub") + if not isinstance(subject, str) or not subject: + return self._error_redirect(frontend, "invalid_token") + try: + user = await self._user_manager.resolve_or_create_sso_user( + provider_id=provider.id, subject=subject, claims=claims + ) + except OctopError as exc: + if exc.code is ErrorCode.USER_DISABLED: + return self._error_redirect(frontend, "disabled") + return self._error_redirect(frontend, "generic") + except Exception: + return self._error_redirect(frontend, "generic") + + login_code = secrets.token_urlsafe(32) + self._services.sso_repo.attach_login_code( + state, + login_code=login_code, + user_id=user.id, + expires_at=int(time.time()) + _LOGIN_CODE_TTL_SECONDS, + ) + # Put credentials in the URL fragment so they are not sent to the server + # or written to typical access logs for the complete page request. + return RedirectResult( + f"{frontend}/login/oidc/complete#" + f"{urlencode({'code': login_code, 'redirect': login_state.redirect_after})}" + ) + + async def exchange_login_code(self, code: str) -> User: + consumed = self._services.sso_repo.consume_login_code(code) + if consumed is None: + raise ValueError("invalid or expired SSO login code") + user = self._user_manager.get_by_id(consumed["user_id"]) + if user is None: + raise ValueError("SSO user is unavailable") + self._services.audit_repo.write( + actor=user.username, action="auth.oidc_login", target=user.username + ) + return user + + def _enabled_provider(self) -> SsoProviderRow: + provider = self._services.sso_repo.get_provider() + if provider is None or not provider.enabled or self._user_manager.count() == 0: + raise ValueError("SSO is disabled") + if not provider.issuer.strip() or not provider.client_id.strip(): + raise ValueError("SSO is misconfigured") + return provider + + def _configured_provider(self) -> SsoProviderRow: + provider = self._services.sso_repo.get_provider() + if provider is None or not provider.issuer.strip() or not provider.client_id.strip(): + raise ValueError("SSO is misconfigured") + return provider + + def _frontend_base(self, public_base: str, *, provider: SsoProviderRow | None = None) -> str: + row = provider if provider is not None else self._services.sso_repo.get_provider() + origin = row.dashboard_origin if row is not None else None + return (origin or public_base).rstrip("/") + + def login_error_frontend(self, public_base: str) -> str: + """Resolve where OIDC error redirects should land (dashboard_origin when set).""" + return self._frontend_base(public_base) + + @staticmethod + def _http_client() -> httpx.Client: + return httpx.Client(timeout=_HTTP_TIMEOUT) + + @staticmethod + def _endpoint(discovery: Mapping[str, object], name: str) -> str: + endpoint = discovery.get(name) + if not isinstance(endpoint, str) or not endpoint: + raise ValueError(f"OIDC discovery has no {name}") + return endpoint + + def _token_request_data( + self, provider: SsoProviderRow, code: str, verifier: str, public_base: str + ) -> dict[str, str]: + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": build_redirect_uri(public_base), + "client_id": provider.client_id, + "code_verifier": verifier, + } + if provider.client_secret_enc is not None: + data["client_secret"] = decrypt_secret( + self._services.secret_repo, provider.client_secret_enc + ) + return data + + def _exchange_claims( + self, + provider: SsoProviderRow, + code: str, + code_verifier: str, + nonce: str, + public_base: str, + ) -> dict[str, Any]: + with self._http_client() as client: + discovery = self._discovery.get(provider.issuer, httpx_client=client) + token_endpoint = self._endpoint(discovery, "token_endpoint") + jwks_uri = self._endpoint(discovery, "jwks_uri") + token_response = client.post( + token_endpoint, + data=self._token_request_data(provider, code, code_verifier, public_base), + ) + token_response.raise_for_status() + token = token_response.json() + id_token = token.get("id_token") if isinstance(token, dict) else None + if not isinstance(id_token, str): + raise jwt.InvalidTokenError("token response has no ID token") + return verify_id_token( + id_token, + jwks_uri=jwks_uri, + issuer=provider.issuer.rstrip("/"), + client_id=provider.client_id, + nonce=nonce, + httpx=client, + ) + + @staticmethod + def _string( + body: Mapping[str, object], + name: str, + current: SsoProviderRow | None, + default: str, + ) -> str: + value = body.get(name) + if isinstance(value, str): + return value.strip() + return str(getattr(current, name)) if current is not None else default + + @staticmethod + def _nullable_string( + body: Mapping[str, object], name: str, current: SsoProviderRow | None + ) -> str | None: + if name in body: + value = body[name] + return value.strip() if isinstance(value, str) and value.strip() else None + return getattr(current, name) if current is not None else None + + @staticmethod + def _provider_config(provider: SsoProviderRow) -> dict[str, Any]: + return { + "enabled": bool(provider.enabled), + "display_name": provider.display_name, + "issuer": provider.issuer, + "client_id": provider.client_id, + "scopes": provider.scopes, + "dashboard_origin": provider.dashboard_origin, + "has_client_secret": provider.client_secret_enc is not None, + } + + @staticmethod + def _error_redirect(frontend_base: str, oidc_error: str) -> RedirectResult: + return RedirectResult( + f"{frontend_base.rstrip('/')}/login?{urlencode({'oidc_error': oidc_error})}" + ) diff --git a/src/octop/infra/backup/__init__.py b/src/octop/infra/backup/__init__.py index 7a2b1c1c..57058925 100644 --- a/src/octop/infra/backup/__init__.py +++ b/src/octop/infra/backup/__init__.py @@ -4,8 +4,10 @@ from octop.infra.backup.store import ( BackupFileInfo, delete_backup_file, + is_auto_backup_filename, list_backup_files, normalize_backup_filename, + prune_auto_backups, read_backup_file, write_backup_file, ) @@ -21,8 +23,10 @@ "delete_backup_file", "export_workspace_zip", "import_workspace_zip", + "is_auto_backup_filename", "list_backup_files", "normalize_backup_filename", + "prune_auto_backups", "read_backup_file", "restore_system_backup", "write_backup_file", diff --git a/src/octop/infra/backup/auto.py b/src/octop/infra/backup/auto.py new file mode 100644 index 00000000..6bb8c998 --- /dev/null +++ b/src/octop/infra/backup/auto.py @@ -0,0 +1,217 @@ +"""Automatic system backup scheduling (process-level CronManager job).""" + +from __future__ import annotations + +import asyncio +import json +import logging +from dataclasses import replace +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +from octop.config import BackupConfig, OctopConfig, load_config +from octop.infra.backup.store import ( + BackupFileInfo, + is_auto_backup_filename, + prune_auto_backups, + write_backup_file, +) +from octop.infra.backup.system_archive import create_system_backup +from octop.infra.cron.trigger import build_trigger +from octop.infra.db.repos.audit import ACTOR_SYSTEM +from octop.infra.errors import ErrorCode, OctopError + +if TYPE_CHECKING: + from octop.config import DatabaseConfig + from octop.infra.cron.manager import CronManager + from octop.infra.db.pool import DatabasePool + from octop.infra.server import OctopServer + from octop.infra.utils.paths import PathLayout + +logger = logging.getLogger(__name__) + +AUTO_BACKUP_JOB_ID = "octop_auto_backup" +_AUTO_FILENAME_PREFIX = "octop-auto-backup-" +_MANUAL_FILENAME_PREFIX = "octop-backup-" + +_lock = asyncio.Lock() + +# Shared with manual create so concurrent backups do not overlap. +BACKUP_LOCK = _lock + + +def to_auto_backup_filename(suggested: str) -> str: + """Map ``octop-backup-*.tar.gz`` → ``octop-auto-backup-*.tar.gz``.""" + name = Path(suggested).name + if is_auto_backup_filename(name): + return name + if name.startswith(_MANUAL_FILENAME_PREFIX): + return _AUTO_FILENAME_PREFIX + name.removeprefix(_MANUAL_FILENAME_PREFIX) + raise OctopError(ErrorCode.SLASH_BAD_ARGS, f"unexpected backup filename: {name!r}") + + +def _atomic_write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + tmp.replace(path) + + +def persist_backup_config(config_path: Path, backup: BackupConfig) -> OctopConfig: + """Merge ``backup`` into ``config.json`` and return the reloaded config.""" + if config_path.exists(): + raw = json.loads(config_path.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + raise OctopError(ErrorCode.INTERNAL_ERROR, "config.json must be a JSON object") + else: + raw = {} + + raw["backup"] = { + "auto_enabled": backup.auto_enabled, + "schedule": backup.schedule, + "retention_count": backup.retention_count, + } + _atomic_write_json(config_path, raw) + return load_config(config_path) + + +def backup_config_from_payload(payload: dict[str, Any]) -> BackupConfig: + """Validate an API/CLI payload into ``BackupConfig``.""" + defaults = BackupConfig() + schedule = str(payload.get("schedule", defaults.schedule)).strip() or defaults.schedule + try: + build_trigger(schedule) + except OctopError as exc: + raise OctopError(ErrorCode.SLASH_BAD_ARGS, f"invalid backup schedule: {schedule}") from exc + try: + retention = int(payload.get("retention_count", defaults.retention_count)) + except (TypeError, ValueError) as exc: + raise OctopError(ErrorCode.SLASH_BAD_ARGS, "retention_count must be an integer") from exc + if retention < 1: + raise OctopError(ErrorCode.SLASH_BAD_ARGS, "retention_count must be >= 1") + return BackupConfig( + auto_enabled=bool(payload.get("auto_enabled", defaults.auto_enabled)), + schedule=schedule, + retention_count=retention, + ) + + +def create_and_store_auto_backup( + *, + paths: PathLayout, + agent_rows: list[Any], + pool: DatabasePool, + db_config: DatabaseConfig, + retention_count: int, +) -> tuple[BackupFileInfo, list[str]]: + """Create a full system backup with the auto filename prefix and prune.""" + data, suggested = create_system_backup( + paths=paths, + agent_rows=agent_rows, + pool=pool, + db_config=db_config, + ) + filename = to_auto_backup_filename(suggested) + entry = write_backup_file(paths, filename, data) + deleted = prune_auto_backups(paths, keep=retention_count) + return entry, deleted + + +async def run_auto_backup(server: OctopServer) -> BackupFileInfo | None: + """Run one automatic backup cycle. Returns ``None`` when skipped (busy/disabled).""" + if _lock.locked(): + logger.info("auto backup skipped: another backup is already running") + return None + + async with _lock: + config = load_config(server.paths.config) + if server.services is None: + logger.warning("auto backup skipped: server services not ready") + return None + + retention = config.backup.retention_count + try: + if server.app_runtime is not None: + agent_rows = cast(list[Any], server.app_runtime.agent_registry.list_rows()) + else: + agent_rows = cast(list[Any], server.services.agent_repo.list_all()) + entry, deleted = await asyncio.to_thread( + create_and_store_auto_backup, + paths=server.paths, + agent_rows=agent_rows, + pool=server.services.db, + db_config=server.services.config.database, + retention_count=retention, + ) + except Exception as exc: + logger.exception("auto backup failed") + try: + server.services.audit_repo.write( + actor=ACTOR_SYSTEM, + action="backup.auto_failed", + target="", + payload=str(exc)[:500], + ) + except Exception: + logger.exception("failed to write backup.auto_failed audit") + return None + + try: + server.services.audit_repo.write( + actor=ACTOR_SYSTEM, + action="backup.auto_ok", + target=entry.name, + payload=json.dumps({"size": entry.size, "pruned": deleted}), + ) + except Exception: + logger.exception("failed to write backup.auto_ok audit") + + logger.info( + "auto backup wrote %s (%d bytes); pruned %d", + entry.name, + entry.size, + len(deleted), + ) + return entry + + +def apply_auto_backup_schedule(cron_manager: CronManager, *, server: OctopServer) -> None: + """Register or remove the auto-backup system job from current config.""" + cron_manager.unschedule_system_job(AUTO_BACKUP_JOB_ID) + config = load_config(server.paths.config) + if not config.backup.auto_enabled: + logger.info("auto backup disabled; system job not scheduled") + return + + schedule = config.backup.schedule + try: + build_trigger(schedule) + except OctopError: + logger.warning("auto backup schedule %r is invalid; not scheduling", schedule) + return + + async def _run() -> None: + await run_auto_backup(server) + + cron_manager.schedule_system_job( + AUTO_BACKUP_JOB_ID, + trigger=schedule, + func=_run, + ) + logger.info("auto backup job scheduled (%s)", schedule) + + +def install_auto_backup_job(cron_manager: CronManager, *, server: OctopServer) -> None: + """Boot-time hook: schedule auto backup when enabled in config.""" + apply_auto_backup_schedule(cron_manager, server=server) + + +def update_server_backup_config(server: OctopServer, backup: BackupConfig) -> OctopConfig: + """Persist backup settings, refresh in-memory config, and reschedule the job.""" + new_config = persist_backup_config(server.paths.config, backup) + server.config = new_config + if server.services is not None: + server.services = replace(server.services, config=new_config) + if server.app_runtime is not None: + apply_auto_backup_schedule(server.app_runtime.cron_manager, server=server) + return new_config diff --git a/src/octop/infra/backup/store.py b/src/octop/infra/backup/store.py index c417e9e1..aa9260c8 100644 --- a/src/octop/infra/backup/store.py +++ b/src/octop/infra/backup/store.py @@ -12,8 +12,9 @@ _BACKUP_SUFFIXES = (".tar.gz", ".tgz") _BACKUP_CREATED_RE = re.compile( - r"octop-backup-(\d{8}T\d{6}Z)", + r"octop-(?:auto-)?backup-(\d{8}T\d{6}Z)", ) +_AUTO_BACKUP_PREFIX = "octop-auto-backup-" def _iso_utc_from_timestamp(ts: float) -> str: @@ -117,3 +118,33 @@ def delete_backup_file(paths: PathLayout, filename: str) -> None: if not path.is_file(): raise OctopError(ErrorCode.NOT_FOUND, f"backup not found: {safe}") path.unlink() + + +def is_auto_backup_filename(name: str) -> bool: + """True when ``name`` is an automatic backup archive (not a manual create).""" + base = Path(name).name + return base.startswith(_AUTO_BACKUP_PREFIX) and any( + base.endswith(suffix) for suffix in _BACKUP_SUFFIXES + ) + + +def prune_auto_backups(paths: PathLayout, *, keep: int) -> list[str]: + """Delete oldest automatic backups beyond ``keep``; return deleted filenames. + + Manual ``octop-backup-*`` archives are never removed. ``keep <= 0`` deletes + all automatic backups. + """ + autos = [f for f in list_backup_files(paths) if is_auto_backup_filename(f.name)] + # list_backup_files is newest-first by mtime; prefer created_at stamp order. + autos.sort(key=lambda f: f.created_at, reverse=True) + if keep < 0: + keep = 0 + to_delete = autos[keep:] + deleted: list[str] = [] + for info in to_delete: + try: + delete_backup_file(paths, info.name) + except OctopError: + continue + deleted.append(info.name) + return deleted diff --git a/src/octop/infra/cron/manager.py b/src/octop/infra/cron/manager.py index a46ff2c6..59ee4474 100644 --- a/src/octop/infra/cron/manager.py +++ b/src/octop/infra/cron/manager.py @@ -274,3 +274,13 @@ def schedule_system_job(self, job_id: str, *, trigger: str, func: Any) -> None: replace_existing=True, misfire_grace_time=300, ) + + def unschedule_system_job(self, job_id: str) -> None: + """Remove a process-level job previously registered via :meth:`schedule_system_job`.""" + self._system_job_ids.discard(job_id) + if self._scheduler.get_job(job_id): + self._scheduler.remove_job(job_id) + + def has_system_job(self, job_id: str) -> bool: + """True when a system job id is currently registered with the scheduler.""" + return self._scheduler.get_job(job_id) is not None diff --git a/src/octop/infra/db/migrate.py b/src/octop/infra/db/migrate.py index be8057f5..719b1bbf 100644 --- a/src/octop/infra/db/migrate.py +++ b/src/octop/infra/db/migrate.py @@ -117,6 +117,206 @@ def _ensure_skill_packages_name_unique_index(db: DatabasePool) -> None: ) +def _ensure_published_experts_table(db: DatabasePool) -> None: + """Create published_experts if missing (idempotent for partial upgrades).""" + if _table_exists(db, "published_experts"): + return + with db.connect() as conn: + conn.execute( + """ + CREATE TABLE published_experts ( + id TEXT PRIMARY KEY, + slug TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + created_by TEXT NOT NULL, + source_agent_id TEXT, + icon_name TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + """ + ) + + +def _ensure_published_experts_indexes(db: DatabasePool) -> None: + if not _table_exists(db, "published_experts"): + return + with db.connect() as conn: + conn.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_published_experts_slug " + "ON published_experts(slug)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_published_experts_created_by " + "ON published_experts(created_by)" + ) + + +def _ensure_knowledge_bases_schema(db: DatabasePool) -> None: + """Create knowledge tables / columns idempotently (renumbered local upgrades).""" + if not _table_exists(db, "knowledge_bases"): + with db.connect() as conn: + conn.execute( + """ + CREATE TABLE knowledge_bases ( + id TEXT PRIMARY KEY, + owner_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + default_open INTEGER NOT NULL DEFAULT 0, + shared INTEGER NOT NULL DEFAULT 0, + icon_name TEXT NOT NULL DEFAULT '', + embedding_model TEXT NOT NULL DEFAULT '', + embedding_dim INTEGER NOT NULL DEFAULT 0, + doc_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(owner_user_id, name) + ) + """ + ) + else: + _ensure_column(db, "knowledge_bases", "shared", "INTEGER NOT NULL DEFAULT 0") + _ensure_column(db, "knowledge_bases", "icon_name", "TEXT NOT NULL DEFAULT ''") + with db.connect() as conn: + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_knowledge_bases_owner ON knowledge_bases(owner_user_id)" + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS knowledge_base_members ( + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (kb_id, user_id) + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS knowledge_documents ( + id TEXT PRIMARY KEY, + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + byte_size INTEGER NOT NULL, + content_hash TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + error_message TEXT NOT NULL DEFAULT '', + chunk_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_knowledge_documents_kb ON knowledge_documents(kb_id)" + ) + + +def _ensure_sso_oidc_schema(db: DatabasePool) -> None: + """Apply OIDC SSO tables and nullable password_hash (SQLite users rebuild).""" + if _table_exists(db, "sso_providers"): + return + with db.connect() as conn: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS sso_providers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled INTEGER NOT NULL DEFAULT 0, + display_name TEXT NOT NULL DEFAULT '', + issuer TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL DEFAULT '', + client_secret_enc BLOB, + scopes TEXT NOT NULL DEFAULT 'openid profile email', + dashboard_origin TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sso_login_states ( + state TEXT PRIMARY KEY, + provider_id INTEGER NOT NULL REFERENCES sso_providers(id) ON DELETE CASCADE, + nonce TEXT NOT NULL, + code_verifier TEXT NOT NULL, + redirect_after TEXT NOT NULL DEFAULT '/chat', + login_code TEXT, + user_id INTEGER, + expires_at INTEGER NOT NULL, + consumed_at INTEGER, + created_at INTEGER NOT NULL + ); + + CREATE UNIQUE INDEX IF NOT EXISTS idx_sso_login_states_login_code + ON sso_login_states(login_code) WHERE login_code IS NOT NULL; + + PRAGMA foreign_keys = OFF; + + CREATE TABLE users_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password_hash TEXT, + role TEXT NOT NULL, + display_name TEXT, + disabled INTEGER NOT NULL DEFAULT 0, + locale TEXT NOT NULL DEFAULT 'zh', + created_at INTEGER NOT NULL, + login_failed_count INTEGER NOT NULL DEFAULT 0, + login_locked_until INTEGER NOT NULL DEFAULT 0, + preferences_json TEXT NOT NULL DEFAULT '{}', + email TEXT, + sso_provider_id INTEGER REFERENCES sso_providers(id), + sso_subject TEXT, + permissions TEXT NOT NULL DEFAULT '[]' + ); + + INSERT INTO users_new ( + id, + username, + password_hash, + role, + display_name, + disabled, + locale, + created_at, + login_failed_count, + login_locked_until, + preferences_json, + permissions + ) + SELECT + id, + username, + password_hash, + role, + display_name, + disabled, + locale, + created_at, + login_failed_count, + login_locked_until, + preferences_json, + '[]' + FROM users; + + DROP TABLE users; + ALTER TABLE users_new RENAME TO users; + + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username); + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email + ON users(email) WHERE email IS NOT NULL AND email != ''; + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_sso + ON users(sso_provider_id, sso_subject) + WHERE sso_provider_id IS NOT NULL AND sso_subject IS NOT NULL; + + PRAGMA foreign_keys = ON; + """ + ) + + def _repair_legacy_schema(db: DatabasePool) -> None: """Idempotent compatibility repairs for local databases from old builds.""" if _table_exists(db, "users"): @@ -124,6 +324,12 @@ def _repair_legacy_schema(db: DatabasePool) -> None: _ensure_column(db, "users", "login_failed_count", "INTEGER NOT NULL DEFAULT 0") _ensure_column(db, "users", "login_locked_until", "INTEGER NOT NULL DEFAULT 0") _ensure_column(db, "users", "preferences_json", "TEXT NOT NULL DEFAULT '{}'") + _ensure_column(db, "users", "email", "TEXT") + _ensure_column(db, "users", "sso_provider_id", "INTEGER") + _ensure_column(db, "users", "sso_subject", "TEXT") + # Pre-squash DBs may already report version ≥6; reconcile can clamp to 6 + # without applying 006_user_permissions.sql — ensure the column here. + _ensure_column(db, "users", "permissions", "TEXT NOT NULL DEFAULT '[]'") if _table_exists(db, "cron_jobs"): _ensure_column( db, @@ -141,11 +347,56 @@ def _repair_legacy_schema(db: DatabasePool) -> None: _ensure_column(db, "threads", "model_ref", "TEXT") _ensure_column(db, "threads", "reasoning_mode", "TEXT") _ensure_column(db, "threads", "reasoning_effort", "TEXT") + if _table_exists(db, "agents"): + _ensure_column(db, "agents", "is_shared", "INTEGER NOT NULL DEFAULT 0") _ensure_skill_packages_table(db) if _table_exists(db, "skill_packages"): _ensure_column(db, "skill_packages", "icon_name", "TEXT NOT NULL DEFAULT ''") _ensure_column(db, "skill_packages", "icon_url", "TEXT NOT NULL DEFAULT ''") _ensure_skill_packages_name_unique_index(db) + _ensure_published_experts_table(db) + _ensure_published_experts_indexes(db) + # Cover pre-squash develop DBs that already recorded version ≥5 but only + # applied a subset of the former 005–009 files (or the old thin 005). + # Require ``users`` first — SSO rebuild and knowledge FKs need it, and a + # brand-new DB has not applied 001 yet when repair runs. + if _table_exists(db, "users"): + _ensure_sso_oidc_schema(db) + _ensure_knowledge_bases_schema(db) + # SSO rebuild recreates ``users``; ensure permissions after that path. + _ensure_column(db, "users", "permissions", "TEXT NOT NULL DEFAULT '[]'") + + +def _max_discovered_version(dialect: str) -> int: + versions = [version for version, _ in _discover(dialect)] + return max(versions) if versions else 0 + + +def _reconcile_pre_squash_schema_version(db: DatabasePool) -> None: + """Clamp develop DBs that applied split 005–009 down to the consolidated max. + + Before squash, local/develop installs could sit at schema versions 6–9. + After those files are merged into a single 005, leaving version > max would + permanently skip future migrations (e.g. a new 006). + """ + current = _current_version(db) + max_version = _max_discovered_version(db.dialect) + if max_version <= 0 or current <= max_version: + return + if db.dialect == "postgresql": + path = next(path for version, path in _discover("postgresql") if version == max_version) + sql = path.read_text(encoding="utf-8") + with db.connect() as conn, conn.transaction(): + _apply_postgresql_migration(conn, sql) + conn.execute("UPDATE _schema_version SET version = %s", (max_version,)) + return + # SQLite: ensure helpers already ran via _repair_legacy_schema; also apply + # the max migration's ensure path so clamping to a new max (e.g. 6) does not + # skip ADD COLUMN for DBs that previously sat at version 7–9. + if max_version == 6 and _table_exists(db, "users"): + _ensure_column(db, "users", "permissions", "TEXT NOT NULL DEFAULT '[]'") + with db.connect() as conn: + conn.execute("UPDATE _schema_version SET version = ?", (max_version,)) def _apply_postgresql_migration(conn: Any, sql: str) -> None: @@ -161,6 +412,10 @@ def _apply_sqlite_migration(db: DatabasePool, version: int, path: Path) -> None: Version 3 bumps schema then rewrites legacy hard-cut thread titles in Python. Version 4 adds composer columns idempotently after legacy schema repair. + Version 5 adds shared experts, published templates, OIDC SSO, and knowledge + bases idempotently after legacy schema repair. + Version 6 adds ``users.permissions`` idempotently (also covered by + ``_repair_legacy_schema`` for DBs whose version was clamped past 006). """ if version == 2: if _table_exists(db, "cron_jobs"): @@ -194,6 +449,27 @@ def _apply_sqlite_migration(db: DatabasePool, version: int, path: Path) -> None: with db.connect() as conn: conn.execute("UPDATE _schema_version SET version = ?", (version,)) return + if version == 5: + if _table_exists(db, "agents"): + _ensure_column(db, "agents", "is_shared", "INTEGER NOT NULL DEFAULT 0") + with db.connect() as conn: + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_agents_shared " + "ON agents(is_shared) WHERE is_shared = 1" + ) + _ensure_published_experts_table(db) + _ensure_published_experts_indexes(db) + _ensure_sso_oidc_schema(db) + _ensure_knowledge_bases_schema(db) + with db.connect() as conn: + conn.execute("UPDATE _schema_version SET version = ?", (version,)) + return + if version == 6: + if _table_exists(db, "users"): + _ensure_column(db, "users", "permissions", "TEXT NOT NULL DEFAULT '[]'") + with db.connect() as conn: + conn.execute("UPDATE _schema_version SET version = ?", (version,)) + return sql = path.read_text(encoding="utf-8") with db.connect() as conn: conn.executescript(sql) @@ -215,3 +491,4 @@ def run_migrations(db: DatabasePool) -> None: repair_all_legacy_thread_titles(db) else: _apply_sqlite_migration(db, version, path) + _reconcile_pre_squash_schema_version(db) diff --git a/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.pg.sql b/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.pg.sql new file mode 100644 index 00000000..9744b2d6 --- /dev/null +++ b/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.pg.sql @@ -0,0 +1,100 @@ +-- Schema v5: shared experts, published templates, OIDC SSO, and knowledge bases. +ALTER TABLE agents ADD COLUMN IF NOT EXISTS is_shared INTEGER NOT NULL DEFAULT 0; +CREATE INDEX IF NOT EXISTS idx_agents_shared ON agents(is_shared) WHERE is_shared = 1; + +CREATE TABLE IF NOT EXISTS published_experts ( + id TEXT PRIMARY KEY, + slug TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + created_by TEXT NOT NULL, + source_agent_id TEXT, + icon_name TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT '', + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_published_experts_slug ON published_experts(slug); +CREATE INDEX IF NOT EXISTS idx_published_experts_created_by ON published_experts(created_by); + +CREATE TABLE IF NOT EXISTS sso_providers ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + display_name TEXT NOT NULL DEFAULT '', + issuer TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL DEFAULT '', + client_secret_enc BYTEA, + scopes TEXT NOT NULL DEFAULT 'openid profile email', + dashboard_origin TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS sso_login_states ( + state TEXT PRIMARY KEY, + provider_id INTEGER NOT NULL REFERENCES sso_providers(id) ON DELETE CASCADE, + nonce TEXT NOT NULL, + code_verifier TEXT NOT NULL, + redirect_after TEXT NOT NULL DEFAULT '/chat', + login_code TEXT, + user_id INTEGER, + expires_at INTEGER NOT NULL, + consumed_at INTEGER, + created_at INTEGER NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_sso_login_states_login_code + ON sso_login_states(login_code) WHERE login_code IS NOT NULL; + +ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL; +ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS sso_provider_id INTEGER REFERENCES sso_providers(id); +ALTER TABLE users ADD COLUMN IF NOT EXISTS sso_subject TEXT; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email + ON users(email) WHERE email IS NOT NULL AND email != ''; +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_sso + ON users(sso_provider_id, sso_subject) + WHERE sso_provider_id IS NOT NULL AND sso_subject IS NOT NULL; + +CREATE TABLE IF NOT EXISTS knowledge_bases ( + id TEXT PRIMARY KEY, + owner_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + default_open INTEGER NOT NULL DEFAULT 0, + shared INTEGER NOT NULL DEFAULT 0, + icon_name TEXT NOT NULL DEFAULT '', + embedding_model TEXT NOT NULL DEFAULT '', + embedding_dim INTEGER NOT NULL DEFAULT 0, + doc_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(owner_user_id, name) +); +CREATE INDEX IF NOT EXISTS idx_knowledge_bases_owner ON knowledge_bases(owner_user_id); + +CREATE TABLE IF NOT EXISTS knowledge_base_members ( + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (kb_id, user_id) +); + +CREATE TABLE IF NOT EXISTS knowledge_documents ( + id TEXT PRIMARY KEY, + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + byte_size INTEGER NOT NULL, + content_hash TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + error_message TEXT NOT NULL DEFAULT '', + chunk_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_knowledge_documents_kb ON knowledge_documents(kb_id); + +UPDATE _schema_version SET version = 5; diff --git a/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.sql b/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.sql new file mode 100644 index 00000000..0ad55f1a --- /dev/null +++ b/src/octop/infra/db/migrations/005_shared_experts_sso_knowledge.sql @@ -0,0 +1,147 @@ +-- Schema v5: shared experts, published templates, OIDC SSO, and knowledge bases. +ALTER TABLE agents ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0; +CREATE INDEX idx_agents_shared ON agents(is_shared) WHERE is_shared = 1; + +CREATE TABLE published_experts ( + id TEXT PRIMARY KEY, + slug TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + created_by TEXT NOT NULL, + source_agent_id TEXT, + icon_name TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +CREATE UNIQUE INDEX idx_published_experts_slug ON published_experts(slug); +CREATE INDEX idx_published_experts_created_by ON published_experts(created_by); + +CREATE TABLE IF NOT EXISTS sso_providers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled INTEGER NOT NULL DEFAULT 0, + display_name TEXT NOT NULL DEFAULT '', + issuer TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL DEFAULT '', + client_secret_enc BLOB, + scopes TEXT NOT NULL DEFAULT 'openid profile email', + dashboard_origin TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS sso_login_states ( + state TEXT PRIMARY KEY, + provider_id INTEGER NOT NULL REFERENCES sso_providers(id) ON DELETE CASCADE, + nonce TEXT NOT NULL, + code_verifier TEXT NOT NULL, + redirect_after TEXT NOT NULL DEFAULT '/chat', + login_code TEXT, + user_id INTEGER, + expires_at INTEGER NOT NULL, + consumed_at INTEGER, + created_at INTEGER NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_sso_login_states_login_code + ON sso_login_states(login_code) WHERE login_code IS NOT NULL; + +PRAGMA foreign_keys = OFF; + +CREATE TABLE users_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password_hash TEXT, + role TEXT NOT NULL, + display_name TEXT, + disabled INTEGER NOT NULL DEFAULT 0, + locale TEXT NOT NULL DEFAULT 'zh', + created_at INTEGER NOT NULL, + login_failed_count INTEGER NOT NULL DEFAULT 0, + login_locked_until INTEGER NOT NULL DEFAULT 0, + preferences_json TEXT NOT NULL DEFAULT '{}', + email TEXT, + sso_provider_id INTEGER REFERENCES sso_providers(id), + sso_subject TEXT +); + +INSERT INTO users_new ( + id, + username, + password_hash, + role, + display_name, + disabled, + locale, + created_at, + login_failed_count, + login_locked_until, + preferences_json +) +SELECT + id, + username, + password_hash, + role, + display_name, + disabled, + locale, + created_at, + login_failed_count, + login_locked_until, + preferences_json +FROM users; + +DROP TABLE users; +ALTER TABLE users_new RENAME TO users; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email + ON users(email) WHERE email IS NOT NULL AND email != ''; +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_sso + ON users(sso_provider_id, sso_subject) + WHERE sso_provider_id IS NOT NULL AND sso_subject IS NOT NULL; + +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS knowledge_bases ( + id TEXT PRIMARY KEY, + owner_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + default_open INTEGER NOT NULL DEFAULT 0, + shared INTEGER NOT NULL DEFAULT 0, + icon_name TEXT NOT NULL DEFAULT '', + embedding_model TEXT NOT NULL DEFAULT '', + embedding_dim INTEGER NOT NULL DEFAULT 0, + doc_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(owner_user_id, name) +); +CREATE INDEX IF NOT EXISTS idx_knowledge_bases_owner ON knowledge_bases(owner_user_id); + +CREATE TABLE IF NOT EXISTS knowledge_base_members ( + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (kb_id, user_id) +); + +CREATE TABLE IF NOT EXISTS knowledge_documents ( + id TEXT PRIMARY KEY, + kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + byte_size INTEGER NOT NULL, + content_hash TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + error_message TEXT NOT NULL DEFAULT '', + chunk_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_knowledge_documents_kb ON knowledge_documents(kb_id); + +UPDATE _schema_version SET version = 5; diff --git a/src/octop/infra/db/migrations/006_user_permissions.pg.sql b/src/octop/infra/db/migrations/006_user_permissions.pg.sql new file mode 100644 index 00000000..0e266c8f --- /dev/null +++ b/src/octop/infra/db/migrations/006_user_permissions.pg.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN permissions JSONB NOT NULL DEFAULT '[]'::jsonb; + +UPDATE _schema_version SET version = 6; diff --git a/src/octop/infra/db/migrations/006_user_permissions.sql b/src/octop/infra/db/migrations/006_user_permissions.sql new file mode 100644 index 00000000..6d3cfa49 --- /dev/null +++ b/src/octop/infra/db/migrations/006_user_permissions.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN permissions TEXT NOT NULL DEFAULT '[]'; + +UPDATE _schema_version SET version = 6; diff --git a/src/octop/infra/db/repos/agents.py b/src/octop/infra/db/repos/agents.py index cfe9ecbe..d5843226 100644 --- a/src/octop/infra/db/repos/agents.py +++ b/src/octop/infra/db/repos/agents.py @@ -26,9 +26,14 @@ class AgentRow: updated_at: int icon: str | None = None template_name: str | None = None + is_shared: int = 0 @classmethod def from_row(cls, r: DbRow) -> AgentRow: + try: + is_shared = int(r["is_shared"]) + except KeyError: + is_shared = 0 return cls( id=r["id"], agent_id=r["agent_id"], @@ -46,6 +51,7 @@ def from_row(cls, r: DbRow) -> AgentRow: updated_at=r["updated_at"], icon=r["icon"], template_name=r["template_name"], + is_shared=is_shared, ) @@ -121,6 +127,24 @@ def set_enabled(self, agent_id: str, enabled: bool) -> None: (bool_int(enabled), now_ts(), agent_id), ) + def set_shared(self, agent_id: str, shared: bool) -> None: + with self._db.transaction() as conn: + conn.execute( + "UPDATE agents SET is_shared = ?, updated_at = ? WHERE agent_id = ?", + (bool_int(shared), now_ts(), agent_id), + ) + + def list_shared(self, *, exclude_user_id: int | None = None) -> list[AgentRow]: + sql = "SELECT * FROM agents WHERE is_shared = 1 AND enabled = 1" + params: list[object] = [] + if exclude_user_id is not None: + sql += " AND user_id != ?" + params.append(exclude_user_id) + sql += " ORDER BY created_at ASC, id ASC" + with self._db.connect() as conn: + rows = conn.execute(sql, params).fetchall() + return map_rows(rows, AgentRow) + def set_state(self, agent_id: str, state: str, *, error: str | None = None) -> None: with self._db.transaction() as conn: conn.execute( diff --git a/src/octop/infra/db/repos/knowledge.py b/src/octop/infra/db/repos/knowledge.py new file mode 100644 index 00000000..fa324d48 --- /dev/null +++ b/src/octop/infra/db/repos/knowledge.py @@ -0,0 +1,380 @@ +"""Knowledge base metadata — bases, ACL members, and document rows.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from octop.infra.db.pool import DatabasePool +from octop.infra.db.repos._base import DbRow, bool_int, map_rows, now_ts, partial_updates +from octop.infra.utils.ulid import new_short_id, new_ulid + + +@dataclass(frozen=True) +class KnowledgeBaseRow: + id: str + owner_user_id: int + name: str + description: str + default_open: bool + shared: bool + icon_name: str + embedding_model: str + embedding_dim: int + doc_count: int + created_at: int + updated_at: int + + @classmethod + def from_row(cls, r: DbRow) -> KnowledgeBaseRow: + return cls( + id=r["id"], + owner_user_id=r["owner_user_id"], + name=r["name"], + description=r["description"], + default_open=bool(r["default_open"]), + shared=bool(r["shared"]), + icon_name=str(r["icon_name"] or ""), + embedding_model=r["embedding_model"], + embedding_dim=r["embedding_dim"], + doc_count=r["doc_count"], + created_at=r["created_at"], + updated_at=r["updated_at"], + ) + + +@dataclass(frozen=True) +class KnowledgeMemberRow: + kb_id: str + user_id: int + role: str + created_at: int + + @classmethod + def from_row(cls, r: DbRow) -> KnowledgeMemberRow: + return cls( + kb_id=r["kb_id"], + user_id=r["user_id"], + role=r["role"], + created_at=r["created_at"], + ) + + +@dataclass(frozen=True) +class KnowledgeDocumentRow: + id: str + kb_id: str + filename: str + content_type: str + byte_size: int + content_hash: str + status: str + error_message: str + chunk_count: int + created_at: int + updated_at: int + + @classmethod + def from_row(cls, r: DbRow) -> KnowledgeDocumentRow: + return cls( + id=r["id"], + kb_id=r["kb_id"], + filename=r["filename"], + content_type=r["content_type"], + byte_size=r["byte_size"], + content_hash=r["content_hash"], + status=r["status"], + error_message=r["error_message"], + chunk_count=r["chunk_count"], + created_at=r["created_at"], + updated_at=r["updated_at"], + ) + + +class KnowledgeRepo: + def __init__(self, db: DatabasePool) -> None: + self._db = db + + def _allocate_base_id(self) -> str: + for _ in range(16): + kb_id = new_short_id() + if self.get_base(kb_id) is None: + return kb_id + raise RuntimeError("failed to allocate unique knowledge base id") + + def create_base( + self, + *, + owner_user_id: int, + name: str, + description: str = "", + default_open: bool = False, + shared: bool = False, + icon_name: str = "", + embedding_model: str = "", + embedding_dim: int = 0, + ) -> KnowledgeBaseRow: + kb_id = self._allocate_base_id() + ts = now_ts() + with self._db.transaction() as conn: + conn.execute( + "INSERT INTO knowledge_bases(" + "id, owner_user_id, name, description, default_open, shared, icon_name, " + "embedding_model, embedding_dim, doc_count, created_at, updated_at" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)", + ( + kb_id, + owner_user_id, + name, + description, + bool_int(default_open), + bool_int(shared), + icon_name, + embedding_model, + embedding_dim, + ts, + ts, + ), + ) + row = self.get_base(kb_id) + if row is None: + raise RuntimeError(f"knowledge base insert failed: {kb_id}") + return row + + def list_visible(self, user_id: int) -> list[KnowledgeBaseRow]: + with self._db.connect() as conn: + rows = conn.execute( + "SELECT kb.* FROM knowledge_bases kb " + "WHERE kb.owner_user_id = ? OR kb.shared = 1 " + "ORDER BY kb.name", + (user_id,), + ).fetchall() + return map_rows(rows, KnowledgeBaseRow) + + def list_all(self) -> list[KnowledgeBaseRow]: + with self._db.connect() as conn: + rows = conn.execute("SELECT * FROM knowledge_bases ORDER BY name").fetchall() + return map_rows(rows, KnowledgeBaseRow) + + def count_bases_for_owner(self, owner_user_id: int) -> int: + with self._db.connect() as conn: + row = conn.execute( + "SELECT COUNT(*) AS c FROM knowledge_bases WHERE owner_user_id = ?", + (owner_user_id,), + ).fetchone() + return int(row["c"]) if row else 0 + + def get_base(self, kb_id: str) -> KnowledgeBaseRow | None: + with self._db.connect() as conn: + r = conn.execute("SELECT * FROM knowledge_bases WHERE id = ?", (kb_id,)).fetchone() + return KnowledgeBaseRow.from_row(r) if r else None + + def update_base( + self, + kb_id: str, + *, + name: str | None = None, + description: str | None = None, + default_open: bool | None = None, + shared: bool | None = None, + icon_name: str | None = None, + embedding_model: str | None = None, + embedding_dim: int | None = None, + doc_count: int | None = None, + ) -> None: + fields, params = partial_updates( + [ + ("name", name), + ("description", description), + ("default_open", bool_int(default_open) if default_open is not None else None), + ("shared", bool_int(shared) if shared is not None else None), + ("icon_name", icon_name), + ("embedding_model", embedding_model), + ("embedding_dim", embedding_dim), + ("doc_count", doc_count), + ] + ) + if not fields: + return + fields.append("updated_at = ?") + params.append(now_ts()) + params.append(kb_id) + with self._db.transaction() as conn: + conn.execute( + f"UPDATE knowledge_bases SET {', '.join(fields)} WHERE id = ?", + params, + ) + + def delete_base(self, kb_id: str) -> None: + with self._db.transaction() as conn: + conn.execute("DELETE FROM knowledge_bases WHERE id = ?", (kb_id,)) + + def set_member(self, kb_id: str, *, user_id: int, role: str) -> None: + ts = now_ts() + with self._db.transaction() as conn: + conn.execute( + "INSERT INTO knowledge_base_members(kb_id, user_id, role, created_at) " + "VALUES (?, ?, ?, ?) " + "ON CONFLICT(kb_id, user_id) DO UPDATE SET role = excluded.role", + (kb_id, user_id, role, ts), + ) + + def list_members(self, kb_id: str) -> list[KnowledgeMemberRow]: + with self._db.connect() as conn: + rows = conn.execute( + "SELECT * FROM knowledge_base_members WHERE kb_id = ? ORDER BY user_id", + (kb_id,), + ).fetchall() + return map_rows(rows, KnowledgeMemberRow) + + def remove_member(self, kb_id: str, user_id: int) -> None: + with self._db.transaction() as conn: + conn.execute( + "DELETE FROM knowledge_base_members WHERE kb_id = ? AND user_id = ?", + (kb_id, user_id), + ) + + def create_document( + self, + *, + kb_id: str, + filename: str, + content_type: str, + byte_size: int, + content_hash: str = "", + status: str = "pending", + max_documents: int | None = None, + ) -> KnowledgeDocumentRow: + doc_id = new_ulid() + ts = now_ts() + with self._db.transaction() as conn: + if max_documents is not None: + cursor = conn.execute( + "UPDATE knowledge_bases SET doc_count = doc_count + 1, updated_at = ? " + "WHERE id = ? AND doc_count < ?", + (ts, kb_id, max_documents), + ) + if cursor.rowcount != 1: + raise ValueError(f"knowledge bases support at most {max_documents} documents") + conn.execute( + "INSERT INTO knowledge_documents(" + "id, kb_id, filename, content_type, byte_size, content_hash, " + "status, error_message, chunk_count, created_at, updated_at" + ") VALUES (?, ?, ?, ?, ?, ?, ?, '', 0, ?, ?)", + (doc_id, kb_id, filename, content_type, byte_size, content_hash, status, ts, ts), + ) + if max_documents is None: + conn.execute( + "UPDATE knowledge_bases SET doc_count = doc_count + 1, updated_at = ? WHERE id = ?", + (ts, kb_id), + ) + row = self.get_document(doc_id) + if row is None: + raise RuntimeError(f"knowledge document insert failed: {doc_id}") + return row + + def list_documents(self, kb_id: str) -> list[KnowledgeDocumentRow]: + with self._db.connect() as conn: + rows = conn.execute( + "SELECT * FROM knowledge_documents WHERE kb_id = ? ORDER BY filename", + (kb_id,), + ).fetchall() + return map_rows(rows, KnowledgeDocumentRow) + + def get_document(self, doc_id: str) -> KnowledgeDocumentRow | None: + with self._db.connect() as conn: + r = conn.execute( + "SELECT * FROM knowledge_documents WHERE id = ?", + (doc_id,), + ).fetchone() + return KnowledgeDocumentRow.from_row(r) if r else None + + def update_document( + self, + doc_id: str, + *, + filename: str | None = None, + content_type: str | None = None, + byte_size: int | None = None, + content_hash: str | None = None, + status: str | None = None, + error_message: str | None = None, + chunk_count: int | None = None, + ) -> None: + fields, params = partial_updates( + [ + ("filename", filename), + ("content_type", content_type), + ("byte_size", byte_size), + ("content_hash", content_hash), + ("status", status), + ("error_message", error_message), + ("chunk_count", chunk_count), + ] + ) + if not fields: + return + fields.append("updated_at = ?") + params.append(now_ts()) + params.append(doc_id) + with self._db.transaction() as conn: + conn.execute( + f"UPDATE knowledge_documents SET {', '.join(fields)} WHERE id = ?", + params, + ) + + def delete_document(self, doc_id: str) -> None: + ts = now_ts() + with self._db.transaction() as conn: + row = conn.execute( + "SELECT kb_id FROM knowledge_documents WHERE id = ?", + (doc_id,), + ).fetchone() + if row is None: + return + kb_id = row["kb_id"] + conn.execute("DELETE FROM knowledge_documents WHERE id = ?", (doc_id,)) + conn.execute( + "UPDATE knowledge_bases SET doc_count = doc_count - 1, updated_at = ? WHERE id = ?", + (ts, kb_id), + ) + + def count_documents(self, kb_id: str) -> int: + with self._db.connect() as conn: + row = conn.execute( + "SELECT COUNT(*) AS n FROM knowledge_documents WHERE kb_id = ?", + (kb_id,), + ).fetchone() + return int(row["n"]) if row else 0 + + def reindex_all_documents(self, embedding_model: str) -> list[KnowledgeDocumentRow]: + """Reset all document work after changing the shared embedding model.""" + ts = now_ts() + with self._db.transaction() as conn: + conn.execute( + "UPDATE knowledge_bases SET embedding_model = ?, embedding_dim = 0, updated_at = ?", + (embedding_model, ts), + ) + conn.execute( + "UPDATE knowledge_documents " + "SET status = 'pending', error_message = '', chunk_count = 0, updated_at = ?", + (ts,), + ) + rows = conn.execute( + "SELECT * FROM knowledge_documents WHERE status = 'pending' ORDER BY kb_id, id" + ).fetchall() + return map_rows(rows, KnowledgeDocumentRow) + + def resume_pending_documents(self) -> list[KnowledgeDocumentRow]: + """Return pending work after resetting jobs interrupted while processing.""" + ts = now_ts() + with self._db.transaction() as conn: + conn.execute( + "UPDATE knowledge_documents " + "SET status = 'pending', error_message = '', updated_at = ? " + "WHERE status = 'processing'", + (ts,), + ) + rows = conn.execute( + "SELECT * FROM knowledge_documents WHERE status = 'pending' ORDER BY kb_id, id" + ).fetchall() + return map_rows(rows, KnowledgeDocumentRow) diff --git a/src/octop/infra/db/repos/published_experts.py b/src/octop/infra/db/repos/published_experts.py new file mode 100644 index 00000000..c8c95a82 --- /dev/null +++ b/src/octop/infra/db/repos/published_experts.py @@ -0,0 +1,147 @@ +"""Published expert templates — one row per user-published expert.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from octop.infra.db.pool import DatabasePool +from octop.infra.db.repos._base import DbRow, map_rows, now_ts + + +@dataclass(frozen=True) +class PublishedExpertRow: + id: str + slug: str + name: str + description: str + created_by: str + source_agent_id: str | None + icon_name: str + color: str + created_at: int + updated_at: int + + @classmethod + def from_row(cls, r: DbRow) -> PublishedExpertRow: + return cls( + id=r["id"], + slug=r["slug"], + name=r["name"], + description=r["description"], + created_by=r["created_by"], + source_agent_id=r["source_agent_id"], + icon_name=r["icon_name"], + color=r["color"], + created_at=int(r["created_at"]), + updated_at=int(r["updated_at"]), + ) + + +class PublishedExpertRepo: + def __init__(self, db: DatabasePool) -> None: + self._db = db + + def list_all(self) -> list[PublishedExpertRow]: + with self._db.connect() as conn: + rows = conn.execute("SELECT * FROM published_experts ORDER BY name").fetchall() + return map_rows(rows, PublishedExpertRow) + + def get(self, expert_id: str) -> PublishedExpertRow | None: + with self._db.connect() as conn: + r = conn.execute( + "SELECT * FROM published_experts WHERE id = ?", + (expert_id,), + ).fetchone() + return PublishedExpertRow.from_row(r) if r else None + + def get_by_slug(self, slug: str) -> PublishedExpertRow | None: + with self._db.connect() as conn: + r = conn.execute( + "SELECT * FROM published_experts WHERE slug = ?", + (slug,), + ).fetchone() + return PublishedExpertRow.from_row(r) if r else None + + def get_by_source_agent_id(self, source_agent_id: str) -> PublishedExpertRow | None: + with self._db.connect() as conn: + r = conn.execute( + "SELECT * FROM published_experts WHERE source_agent_id = ? ORDER BY created_at LIMIT 1", + (source_agent_id,), + ).fetchone() + return PublishedExpertRow.from_row(r) if r else None + + def create( + self, + *, + id: str, + slug: str, + name: str, + description: str = "", + created_by: str, + source_agent_id: str | None = None, + icon_name: str = "", + color: str = "", + ) -> PublishedExpertRow: + ts = now_ts() + with self._db.transaction() as conn: + conn.execute( + "INSERT INTO published_experts(" + "id, slug, name, description, created_by, source_agent_id, " + "icon_name, color, created_at, updated_at" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + id, + slug, + name, + description, + created_by, + source_agent_id, + icon_name, + color, + ts, + ts, + ), + ) + row = self.get(id) + if row is None: + raise RuntimeError(f"published expert insert failed: {id}") + return row + + def update_snapshot_meta( + self, + expert_id: str, + *, + icon_name: str | None = None, + color: str | None = None, + description: str | None = None, + name: str | None = None, + ) -> PublishedExpertRow: + """Refresh listing metadata and bump ``updated_at`` after a snapshot rewrite.""" + fields: list[str] = ["updated_at = ?"] + values: list[object] = [now_ts()] + if icon_name is not None: + fields.append("icon_name = ?") + values.append(icon_name) + if color is not None: + fields.append("color = ?") + values.append(color) + if description is not None: + fields.append("description = ?") + values.append(description) + if name is not None: + fields.append("name = ?") + values.append(name) + values.append(expert_id) + with self._db.transaction() as conn: + conn.execute( + f"UPDATE published_experts SET {', '.join(fields)} WHERE id = ?", + tuple(values), + ) + row = self.get(expert_id) + if row is None: + raise RuntimeError(f"published expert update failed: {expert_id}") + return row + + def delete(self, expert_id: str) -> None: + with self._db.transaction() as conn: + conn.execute("DELETE FROM published_experts WHERE id = ?", (expert_id,)) diff --git a/src/octop/infra/db/repos/sso.py b/src/octop/infra/db/repos/sso.py new file mode 100644 index 00000000..9b1492c4 --- /dev/null +++ b/src/octop/infra/db/repos/sso.py @@ -0,0 +1,200 @@ +"""OIDC SSO provider and login-state table access.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from octop.infra.db.pool import DatabasePool +from octop.infra.db.repos._base import DbRow, bool_int, insert_returning_id, now_ts + + +@dataclass(frozen=True) +class SsoProviderRow: + id: int + enabled: int + display_name: str + issuer: str + client_id: str + client_secret_enc: bytes | None + scopes: str + dashboard_origin: str | None + created_at: int + updated_at: int + + @classmethod + def from_row(cls, row: DbRow) -> SsoProviderRow: + secret = row["client_secret_enc"] + return cls( + id=int(row["id"]), + enabled=int(row["enabled"]), + display_name=str(row["display_name"]), + issuer=str(row["issuer"]), + client_id=str(row["client_id"]), + client_secret_enc=bytes(secret) if secret is not None else None, + scopes=str(row["scopes"]), + dashboard_origin=row["dashboard_origin"], + created_at=int(row["created_at"]), + updated_at=int(row["updated_at"]), + ) + + +@dataclass(frozen=True) +class SsoLoginStateRow: + state: str + provider_id: int + nonce: str + code_verifier: str + redirect_after: str + login_code: str | None + user_id: int | None + expires_at: int + consumed_at: int | None + created_at: int + + @classmethod + def from_row(cls, row: DbRow) -> SsoLoginStateRow: + return cls( + state=str(row["state"]), + provider_id=int(row["provider_id"]), + nonce=str(row["nonce"]), + code_verifier=str(row["code_verifier"]), + redirect_after=str(row["redirect_after"]), + login_code=row["login_code"], + user_id=int(row["user_id"]) if row["user_id"] is not None else None, + expires_at=int(row["expires_at"]), + consumed_at=int(row["consumed_at"]) if row["consumed_at"] is not None else None, + created_at=int(row["created_at"]), + ) + + +class SsoRepo: + def __init__(self, db: DatabasePool) -> None: + self._db = db + + def get_provider(self) -> SsoProviderRow | None: + with self._db.connect() as conn: + row = conn.execute("SELECT * FROM sso_providers ORDER BY id LIMIT 1").fetchone() + return SsoProviderRow.from_row(row) if row else None + + def upsert_provider( + self, + *, + enabled: bool, + display_name: str, + issuer: str, + client_id: str, + client_secret_enc: bytes | None, + scopes: str, + dashboard_origin: str | None, + ) -> SsoProviderRow: + ts = now_ts() + with self._db.transaction() as conn: + existing = conn.execute("SELECT * FROM sso_providers ORDER BY id LIMIT 1").fetchone() + if existing is None: + provider_id = insert_returning_id( + conn, + "INSERT INTO sso_providers(" + "enabled, display_name, issuer, client_id, client_secret_enc, scopes, " + "dashboard_origin, created_at, updated_at" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + bool_int(enabled), + display_name, + issuer, + client_id, + client_secret_enc, + scopes, + dashboard_origin, + ts, + ts, + ), + ) + else: + provider_id = int(existing["id"]) + fields = [ + "enabled = ?", + "display_name = ?", + "issuer = ?", + "client_id = ?", + "scopes = ?", + "dashboard_origin = ?", + "updated_at = ?", + ] + params: list[object] = [ + bool_int(enabled), + display_name, + issuer, + client_id, + scopes, + dashboard_origin, + ts, + ] + if client_secret_enc is not None: + fields.insert(4, "client_secret_enc = ?") + params.insert(4, client_secret_enc) + conn.execute( + f"UPDATE sso_providers SET {', '.join(fields)} WHERE id = ?", + (*params, provider_id), + ) + row = conn.execute( + "SELECT * FROM sso_providers WHERE id = ?", (provider_id,) + ).fetchone() + if row is None: + raise RuntimeError("SSO provider upsert returned no row") + return SsoProviderRow.from_row(row) + + def create_login_state( + self, + *, + state: str, + provider_id: int, + nonce: str, + code_verifier: str, + redirect_after: str, + expires_at: int, + ) -> None: + with self._db.transaction() as conn: + conn.execute( + "INSERT INTO sso_login_states(" + "state, provider_id, nonce, code_verifier, redirect_after, expires_at, created_at" + ") VALUES (?, ?, ?, ?, ?, ?, ?)", + (state, provider_id, nonce, code_verifier, redirect_after, expires_at, now_ts()), + ) + + def take_login_state(self, state: str) -> SsoLoginStateRow | None: + """Atomically claim a login state for one callback attempt.""" + with self._db.transaction() as conn: + row = conn.execute( + "UPDATE sso_login_states SET consumed_at = ? " + "WHERE state = ? AND consumed_at IS NULL AND expires_at > ? " + "AND login_code IS NULL RETURNING *", + (now_ts(), state, now_ts()), + ).fetchone() + return SsoLoginStateRow.from_row(row) if row else None + + def attach_login_code(self, state: str, login_code: str, user_id: int, expires_at: int) -> None: + """Attach a one-time login code after a claimed callback succeeds. + + Clears ``consumed_at`` so ``consume_login_code`` can finalize the session. + """ + with self._db.transaction() as conn: + conn.execute( + "UPDATE sso_login_states SET login_code = ?, user_id = ?, expires_at = ?, " + "consumed_at = NULL WHERE state = ? AND login_code IS NULL", + (login_code, user_id, expires_at, state), + ) + + def consume_login_code(self, login_code: str) -> dict[str, int] | None: + with self._db.transaction() as conn: + row = conn.execute( + "UPDATE sso_login_states SET consumed_at = ? " + "WHERE login_code = ? AND consumed_at IS NULL AND expires_at > ? " + "AND user_id IS NOT NULL RETURNING user_id", + (now_ts(), login_code, now_ts()), + ).fetchone() + return {"user_id": int(row["user_id"])} if row else None + + def delete_expired(self, now: int | None = None) -> None: + ts = now if now is not None else now_ts() + with self._db.transaction() as conn: + conn.execute("DELETE FROM sso_login_states WHERE expires_at <= ?", (ts,)) diff --git a/src/octop/infra/db/repos/threads.py b/src/octop/infra/db/repos/threads.py index f0c9e9b5..e9d8a0a7 100644 --- a/src/octop/infra/db/repos/threads.py +++ b/src/octop/infra/db/repos/threads.py @@ -155,6 +155,19 @@ def list_by_agent(self, *, agent_id: str, limit: int = 50) -> list[ThreadRow]: ).fetchall() return map_rows(rows, ThreadRow) + def list_by_agent_user( + self, *, agent_id: str, user_id: int, limit: int = 50 + ) -> list[ThreadRow]: + with self._db.connect() as conn: + rows = conn.execute( + "SELECT * FROM threads WHERE agent_id = ? AND user_id = ? " + "ORDER BY pinned DESC, " + "CASE WHEN last_active > 0 THEN last_active ELSE created_at END DESC, " + "thread_id DESC LIMIT ?", + (agent_id, user_id, limit), + ).fetchall() + return map_rows(rows, ThreadRow) + def list_by_session(self, *, session_key: str, limit: int = 50) -> list[ThreadRow]: with self._db.connect() as conn: rows = conn.execute( diff --git a/src/octop/infra/db/repos/users.py b/src/octop/infra/db/repos/users.py index 536de94e..3b2ede3d 100644 --- a/src/octop/infra/db/repos/users.py +++ b/src/octop/infra/db/repos/users.py @@ -2,28 +2,51 @@ from __future__ import annotations -from dataclasses import dataclass +import builtins +import json +from dataclasses import dataclass, field from octop.infra.db.pool import DatabasePool from octop.infra.db.repos._base import DbRow, bool_int, insert_returning_id, map_rows, now_ts +def _parse_permissions(raw: object) -> builtins.list[str]: + if raw is None: + return [] + if isinstance(raw, list): + return [str(x) for x in raw] + if isinstance(raw, str): + try: + parsed = json.loads(raw or "[]") + except (ValueError, TypeError): + return [] + if isinstance(parsed, list): + return [str(x) for x in parsed] + return [] + return [] + + @dataclass(frozen=True) class UserRow: id: int username: str - password_hash: str + password_hash: str | None role: str display_name: str | None disabled: int created_at: int locale: str + email: str | None + sso_provider_id: int | None + sso_subject: str | None preferences_json: str = "{}" login_failed_count: int = 0 login_locked_until: int = 0 + permissions: builtins.list[str] = field(default_factory=list) @classmethod def from_row(cls, r: DbRow) -> UserRow: + keys = set(r.keys()) return cls( id=r["id"], username=r["username"], @@ -36,6 +59,10 @@ def from_row(cls, r: DbRow) -> UserRow: preferences_json=str(r["preferences_json"] or "{}"), login_failed_count=int(r["login_failed_count"] or 0), login_locked_until=int(r["login_locked_until"] or 0), + email=r["email"] if "email" in keys else None, + sso_provider_id=r["sso_provider_id"] if "sso_provider_id" in keys else None, + sso_subject=r["sso_subject"] if "sso_subject" in keys else None, + permissions=_parse_permissions(r["permissions"] if "permissions" in keys else None), ) @@ -47,17 +74,34 @@ def create( self, *, username: str, - password_hash: str, + password_hash: str | None = None, role: str, display_name: str | None = None, locale: str = "zh", + email: str | None = None, + sso_provider_id: int | None = None, + sso_subject: str | None = None, + permissions: builtins.list[str] | None = None, ) -> int: + perms_json = json.dumps(permissions or [], ensure_ascii=False) with self._db.transaction() as conn: return insert_returning_id( conn, - "INSERT INTO users(username, password_hash, role, display_name, " - "locale, disabled, created_at) VALUES (?, ?, ?, ?, ?, 0, ?)", - (username, password_hash, role, display_name, locale, now_ts()), + "INSERT INTO users(username, password_hash, role, display_name, locale, " + "email, sso_provider_id, sso_subject, disabled, created_at, permissions) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)", + ( + username, + password_hash, + role, + display_name, + locale, + email, + sso_provider_id, + sso_subject, + now_ts(), + perms_json, + ), ) def get(self, user_id: int) -> UserRow | None: @@ -70,7 +114,41 @@ def get_by_username(self, username: str) -> UserRow | None: r = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone() return UserRow.from_row(r) if r else None - def list(self, *, include_disabled: bool = False) -> list[UserRow]: + def get_by_sso(self, provider_id: int, subject: str) -> UserRow | None: + with self._db.connect() as conn: + r = conn.execute( + "SELECT * FROM users WHERE sso_provider_id = ? AND sso_subject = ?", + (provider_id, subject), + ).fetchone() + return UserRow.from_row(r) if r else None + + def get_by_email(self, email: str) -> UserRow | None: + with self._db.connect() as conn: + r = conn.execute("SELECT * FROM users WHERE email = ?", (email,)).fetchone() + return UserRow.from_row(r) if r else None + + def update_sso_profile( + self, + user_id: int, + *, + email: str | None = None, + display_name: str | None = None, + ) -> None: + fields: list[str] = [] + params: list[object] = [] + if email is not None: + fields.append("email = ?") + params.append(email) + if display_name is not None: + fields.append("display_name = ?") + params.append(display_name) + if not fields: + return + params.append(user_id) + with self._db.transaction() as conn: + conn.execute(f"UPDATE users SET {', '.join(fields)} WHERE id = ?", params) + + def list(self, *, include_disabled: bool = False) -> builtins.list[UserRow]: sql = "SELECT * FROM users" if not include_disabled: sql += " WHERE disabled = 0" @@ -115,6 +193,14 @@ def set_preferences_json(self, user_id: int, preferences_json: str) -> None: (preferences_json, user_id), ) + def set_permissions(self, user_id: int, permissions: builtins.list[str]) -> None: + payload = json.dumps(permissions, ensure_ascii=False) + with self._db.transaction() as conn: + conn.execute( + "UPDATE users SET permissions = ? WHERE id = ?", + (payload, user_id), + ) + def delete(self, user_id: int) -> None: with self._db.transaction() as conn: conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) diff --git a/src/octop/infra/db/services.py b/src/octop/infra/db/services.py index 94e58b48..4814d93f 100644 --- a/src/octop/infra/db/services.py +++ b/src/octop/infra/db/services.py @@ -13,12 +13,15 @@ from octop.infra.db.repos.channels import ChannelRepo from octop.infra.db.repos.connectors import ConnectorRepo from octop.infra.db.repos.cron import CronJobRepo +from octop.infra.db.repos.knowledge import KnowledgeRepo from octop.infra.db.repos.proactive_care_config import ProactiveCareConfigRepo from octop.infra.db.repos.providers import ProviderRepo +from octop.infra.db.repos.published_experts import PublishedExpertRepo from octop.infra.db.repos.secrets import SecretRepo from octop.infra.db.repos.sessions import SessionRepo from octop.infra.db.repos.settings import SettingsRepo from octop.infra.db.repos.skill_packages import SkillPackageRepo +from octop.infra.db.repos.sso import SsoRepo from octop.infra.db.repos.threads import ThreadRepo from octop.infra.db.repos.usage import UsageRepo from octop.infra.db.repos.users import UserRepo @@ -44,9 +47,12 @@ class RepoBundle: storage_backend_repo: BackendRepo connector_repo: ConnectorRepo skill_package_repo: SkillPackageRepo + published_expert_repo: PublishedExpertRepo + knowledge_repo: KnowledgeRepo voice_provider_repo: VoiceProviderRepo care_push_repo: CarePushRepo proactive_care_config_repo: ProactiveCareConfigRepo + sso_repo: SsoRepo @classmethod def from_pool(cls, db: DatabasePool) -> RepoBundle: @@ -66,9 +72,12 @@ def from_pool(cls, db: DatabasePool) -> RepoBundle: storage_backend_repo=BackendRepo(db), connector_repo=ConnectorRepo(db), skill_package_repo=SkillPackageRepo(db), + published_expert_repo=PublishedExpertRepo(db), + knowledge_repo=KnowledgeRepo(db), voice_provider_repo=VoiceProviderRepo(db), care_push_repo=CarePushRepo(db), proactive_care_config_repo=ProactiveCareConfigRepo(db), + sso_repo=SsoRepo(db), ) @@ -138,6 +147,14 @@ def connector_repo(self) -> ConnectorRepo: def skill_package_repo(self) -> SkillPackageRepo: return self.repos.skill_package_repo + @property + def published_expert_repo(self) -> PublishedExpertRepo: + return self.repos.published_expert_repo + + @property + def knowledge_repo(self) -> KnowledgeRepo: + return self.repos.knowledge_repo + @property def voice_provider_repo(self) -> VoiceProviderRepo: return self.repos.voice_provider_repo @@ -150,6 +167,10 @@ def care_push_repo(self) -> CarePushRepo: def proactive_care_config_repo(self) -> ProactiveCareConfigRepo: return self.repos.proactive_care_config_repo + @property + def sso_repo(self) -> SsoRepo: + return self.repos.sso_repo + def build_shared_services( *, db: DatabasePool, paths: PathLayout, config: OctopConfig diff --git a/src/octop/infra/desktop/setup.py b/src/octop/infra/desktop/setup.py index 1c2acbdc..70a6ca07 100644 --- a/src/octop/infra/desktop/setup.py +++ b/src/octop/infra/desktop/setup.py @@ -18,6 +18,12 @@ from octop.i18n import tr from octop.infra.utils.posix_compat import geteuid +from octop.infra.utils.runtime_packages import ( + build_install_commands, + build_uninstall_command, +) + +_DESKTOP_PYTHON_PACKAGES = ("mss>=9.0", "pynput>=1.7", "pillow>=10.0") _SUBPROCESS_READ_CHUNK = 4096 _SUBPROCESS_MAX_LINE = 512 * 1024 @@ -341,24 +347,13 @@ def apply_geometry(geometry: str) -> None: def python_deps_install_cmd() -> list[str] | None: """Build install command for desktop optional Python packages.""" - packages = ["mss>=9.0", "pynput>=1.7", "pillow>=10.0"] - uv_bin = shutil.which("uv") - if uv_bin: - return [uv_bin, "pip", "install", "--python", sys.executable, *packages] - if importlib.util.find_spec("pip") is not None: - return [sys.executable, "-m", "pip", "install", *packages] - return None + commands = build_install_commands(_DESKTOP_PYTHON_PACKAGES) + return commands[0] if commands else None def python_deps_uninstall_cmd() -> list[str] | None: """Build uninstall command for desktop optional Python packages.""" - packages = ["mss", "pynput", "pillow"] - uv_bin = shutil.which("uv") - if uv_bin: - return [uv_bin, "pip", "uninstall", "-y", *packages, "--python", sys.executable] - if importlib.util.find_spec("pip") is not None: - return [sys.executable, "-m", "pip", "uninstall", "-y", *packages] - return None + return build_uninstall_command(("mss", "pynput", "pillow")) @dataclass(frozen=True) @@ -822,12 +817,12 @@ async def install_python_deps_stream(locale: str) -> AsyncIterator[str]: if build_deps_failed: return + yield _sse({"log": _install_log(locale, "install_log_deps")}) cmd = python_deps_install_cmd() if cmd is None: async for chunk in _install_failure(_install_log(locale, "error_pip_unavailable")): yield chunk return - yield _sse({"log": _install_log(locale, "install_log_deps")}) run = _SubprocessRun() async for chunk in _stream_subprocess(cmd, emit_done=False, result=run, locale=locale): yield chunk diff --git a/src/octop/infra/errors.py b/src/octop/infra/errors.py index 1aa1d429..dea8315d 100644 --- a/src/octop/infra/errors.py +++ b/src/octop/infra/errors.py @@ -16,6 +16,7 @@ class ErrorCode(StrEnum): SETUP_REQUIRED = "SETUP_REQUIRED" DATABASE_NOT_EMPTY = "DATABASE_NOT_EMPTY" BACKUP_DRIVER_MISMATCH = "BACKUP_DRIVER_MISMATCH" + BACKUP_IN_PROGRESS = "BACKUP_IN_PROGRESS" FORBIDDEN = "FORBIDDEN" NOT_FOUND = "NOT_FOUND" USER_DISABLED = "USER_DISABLED" @@ -34,6 +35,7 @@ class ErrorCode(StrEnum): PROVIDER_NAME_TAKEN = "PROVIDER_NAME_TAKEN" PROVIDER_NOT_VISIBLE = "PROVIDER_NOT_VISIBLE" PROVIDER_REFERENCED = "PROVIDER_REFERENCED" + PROVIDER_LOCAL_PROTECTED = "PROVIDER_LOCAL_PROTECTED" STORAGE_BACKEND_NAME_TAKEN = "STORAGE_BACKEND_NAME_TAKEN" STORAGE_BACKEND_REFERENCED = "STORAGE_BACKEND_REFERENCED" CHANNEL_KIND_UNSUPPORTED = "CHANNEL_KIND_UNSUPPORTED" @@ -64,6 +66,9 @@ class ErrorCode(StrEnum): SKILL_PACKAGE_NOT_FOUND = "SKILL_PACKAGE_NOT_FOUND" SKILL_PACKAGE_NAME_TAKEN = "SKILL_PACKAGE_NAME_TAKEN" SKILL_PACKAGE_BACKEND_UNSUPPORTED = "SKILL_PACKAGE_BACKEND_UNSUPPORTED" + PUBLISHED_EXPERT_SLUG_TAKEN = "PUBLISHED_EXPERT_SLUG_TAKEN" + PUBLISHED_EXPERT_ALREADY_EXISTS = "PUBLISHED_EXPERT_ALREADY_EXISTS" + OIDC_BAD_REQUEST = "OIDC_BAD_REQUEST" EXPERT_MARKET_FAILED = "EXPERT_MARKET_FAILED" SKILLHUB_SSL_FAILED = "SKILLHUB_SSL_FAILED" DESKTOP_SESSION_LIMIT = "DESKTOP_SESSION_LIMIT" @@ -71,6 +76,15 @@ class ErrorCode(StrEnum): PLUGIN_INVALID_ARCHIVE = "PLUGIN_INVALID_ARCHIVE" PLUGIN_INSTALL_FAILED = "PLUGIN_INSTALL_FAILED" PLUGIN_ALREADY_EXISTS = "PLUGIN_ALREADY_EXISTS" + KNOWLEDGE_FEATURE_DISABLED = "KNOWLEDGE_FEATURE_DISABLED" + KNOWLEDGE_PREREQUISITES_FAILED = "KNOWLEDGE_PREREQUISITES_FAILED" + KNOWLEDGE_NOT_FOUND = "KNOWLEDGE_NOT_FOUND" + KNOWLEDGE_FORBIDDEN = "KNOWLEDGE_FORBIDDEN" + KNOWLEDGE_DOC_LIMIT = "KNOWLEDGE_DOC_LIMIT" + KNOWLEDGE_DOC_TOO_LARGE = "KNOWLEDGE_DOC_TOO_LARGE" + KNOWLEDGE_BASE_LIMIT = "KNOWLEDGE_BASE_LIMIT" + KNOWLEDGE_UNSUPPORTED_TYPE = "KNOWLEDGE_UNSUPPORTED_TYPE" + KNOWLEDGE_NAME_TAKEN = "KNOWLEDGE_NAME_TAKEN" _DEFAULT_STATUS: dict[ErrorCode, int] = { @@ -79,6 +93,7 @@ class ErrorCode(StrEnum): ErrorCode.SETUP_REQUIRED: 409, ErrorCode.DATABASE_NOT_EMPTY: 409, ErrorCode.BACKUP_DRIVER_MISMATCH: 400, + ErrorCode.BACKUP_IN_PROGRESS: 409, ErrorCode.FORBIDDEN: 403, ErrorCode.NOT_FOUND: 404, ErrorCode.USER_DISABLED: 403, @@ -97,6 +112,7 @@ class ErrorCode(StrEnum): ErrorCode.PROVIDER_NAME_TAKEN: 409, ErrorCode.PROVIDER_NOT_VISIBLE: 400, ErrorCode.PROVIDER_REFERENCED: 409, + ErrorCode.PROVIDER_LOCAL_PROTECTED: 409, ErrorCode.STORAGE_BACKEND_NAME_TAKEN: 409, ErrorCode.STORAGE_BACKEND_REFERENCED: 409, ErrorCode.CHANNEL_KIND_UNSUPPORTED: 400, @@ -127,6 +143,9 @@ class ErrorCode(StrEnum): ErrorCode.SKILL_PACKAGE_NOT_FOUND: 404, ErrorCode.SKILL_PACKAGE_NAME_TAKEN: 409, ErrorCode.SKILL_PACKAGE_BACKEND_UNSUPPORTED: 400, + ErrorCode.PUBLISHED_EXPERT_SLUG_TAKEN: 409, + ErrorCode.PUBLISHED_EXPERT_ALREADY_EXISTS: 409, + ErrorCode.OIDC_BAD_REQUEST: 400, ErrorCode.EXPERT_MARKET_FAILED: 502, ErrorCode.SKILLHUB_SSL_FAILED: 502, ErrorCode.DESKTOP_SESSION_LIMIT: 429, @@ -134,6 +153,15 @@ class ErrorCode(StrEnum): ErrorCode.PLUGIN_INVALID_ARCHIVE: 400, ErrorCode.PLUGIN_INSTALL_FAILED: 502, ErrorCode.PLUGIN_ALREADY_EXISTS: 409, + ErrorCode.KNOWLEDGE_FEATURE_DISABLED: 403, + ErrorCode.KNOWLEDGE_PREREQUISITES_FAILED: 409, + ErrorCode.KNOWLEDGE_NOT_FOUND: 404, + ErrorCode.KNOWLEDGE_FORBIDDEN: 403, + ErrorCode.KNOWLEDGE_DOC_LIMIT: 409, + ErrorCode.KNOWLEDGE_DOC_TOO_LARGE: 413, + ErrorCode.KNOWLEDGE_BASE_LIMIT: 409, + ErrorCode.KNOWLEDGE_UNSUPPORTED_TYPE: 400, + ErrorCode.KNOWLEDGE_NAME_TAKEN: 409, } diff --git a/src/octop/infra/gateway/gateway.py b/src/octop/infra/gateway/gateway.py index d3b03b3d..13c0f6f7 100644 --- a/src/octop/infra/gateway/gateway.py +++ b/src/octop/infra/gateway/gateway.py @@ -22,6 +22,10 @@ from octop.infra.gateway.cli import CLI_CHANNEL_ID, CliChannel, CliHub from octop.infra.gateway.process import build_harness_request, media_backend_for_agent from octop.infra.gateway.process.processor import GlobalProcessor +from octop.infra.gateway.process.response_mode import ( + normalize_channel_response_mode, + processor_for_response_mode, +) from octop.infra.gateway.slash.dispatcher import SlashDispatcher, build_default_dispatcher from octop.infra.gateway.threads import ThreadRegistry from octop.infra.gateway.ws import WS_CHANNEL_ID, WebSocketChannel, WebSocketHub @@ -186,6 +190,9 @@ async def boot(self) -> None: agent_repo=self._repos.agent_repo, user_repo=self._repos.user_repo, connector_repo=self._repos.connector_repo, + knowledge_repo=self._repos.knowledge_repo, + settings_repo=self._repos.settings_repo, + provider_repo=self._repos.provider_repo, dispatcher=self._dispatcher, usage_repo=self._repos.usage_repo, gateway=self, @@ -613,13 +620,15 @@ async def _register_channel(self, row: ChannelRow) -> None: if not self._channel_manager or not self._processor: return config = self._config_from_row(row) + response_mode = normalize_channel_response_mode(config.get("response_mode")) + processor = processor_for_response_mode(self._processor, response_mode) manager = self._require_channel_manager() await manager.add_channel( row.kind, config, tenant_id=row.agent_id, channel_id=row.channel_id, - processor=self._processor, + processor=processor, ) registered = manager.get_channel(row.channel_id) if registered is not None: diff --git a/src/octop/infra/gateway/media/backend_files.py b/src/octop/infra/gateway/media/backend_files.py index 03779dc3..6fc10b53 100644 --- a/src/octop/infra/gateway/media/backend_files.py +++ b/src/octop/infra/gateway/media/backend_files.py @@ -138,6 +138,9 @@ def is_allowed_host_temp_path(resolved: Path) -> bool: {"image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp", "image/svg+xml"} ) _PREVIEW_VIDEO = frozenset({"video/mp4", "video/webm", "video/quicktime", "video/ogg"}) +_PREVIEW_AUDIO = frozenset( + {"audio/mpeg", "audio/wav", "audio/webm", "audio/ogg", "audio/flac", "audio/aac", "audio/x-wav"} +) def file_url_to_abs_path(file_url: str) -> str: @@ -285,7 +288,7 @@ def _guess_mime(path: str, hint: str = "") -> str: def is_previewable_mime(mime: str) -> bool: base = mime.split(";", 1)[0].strip().lower() - return base in _PREVIEW_IMAGE or base in _PREVIEW_VIDEO + return base in _PREVIEW_IMAGE or base in _PREVIEW_VIDEO or base in _PREVIEW_AUDIO def _abs_path_allowed(abs_path: str, *, workspace: Path) -> bool: diff --git a/src/octop/infra/gateway/process/processor.py b/src/octop/infra/gateway/process/processor.py index 49d8c653..96318aa7 100644 --- a/src/octop/infra/gateway/process/processor.py +++ b/src/octop/infra/gateway/process/processor.py @@ -4,6 +4,7 @@ import logging from collections.abc import AsyncIterator +from types import SimpleNamespace from typing import TYPE_CHECKING, Any from harness_agent.slash import SlashSink, parse_slash @@ -50,6 +51,8 @@ from octop.infra.gateway.process.usage_record import UsageTracker, record_turn_usage from octop.infra.gateway.slash.ctx import SlashCtx, build_slash_ctx from octop.infra.gateway.slash.runner import try_handle_slash +from octop.infra.knowledge.default_open import merge_knowledge_base_ids +from octop.infra.knowledge.hint import catalog_for_selected_bases from octop.infra.users.preferences import ( get_model_reasoning_from_json, get_preferred_model_from_json, @@ -94,6 +97,9 @@ def __init__( agent_repo: AgentRepo, user_repo: UserRepo, connector_repo: ConnectorRepo, + knowledge_repo: Any | None = None, + settings_repo: Any | None = None, + provider_repo: Any | None = None, dispatcher: SlashDispatcher, usage_repo: Any | None = None, gateway: Any | None = None, @@ -105,6 +111,15 @@ def __init__( self._agent_repo = agent_repo self._user_repo = user_repo self._connector_repo = connector_repo + self._knowledge_services = ( + SimpleNamespace( + knowledge_repo=knowledge_repo, + settings_repo=settings_repo, + provider_repo=provider_repo, + ) + if knowledge_repo is not None and settings_repo is not None + else None + ) self._dispatcher = dispatcher self._usage_repo = usage_repo self._gateway = gateway @@ -445,6 +460,13 @@ async def __call__(self, msg: InboundMessage) -> AsyncIterator[MessageEvent]: model=model_ref, message_kwargs=message_kwargs, ) + self._attach_turn_knowledge_config( + request, + user_id=user_id, + is_admin=False, + explicit_ids=None, + locale=locale, + ) if mcp_servers: request["mcp_servers"] = mcp_servers @@ -720,6 +742,15 @@ async def _build_dashboard_request( message_kwargs=message_kwargs or None, reasoning_overrides=reasoning_overrides, ) + self._attach_turn_knowledge_config( + request, + user_id=user_id, + is_admin=bool(meta.get("user_is_admin")), + explicit_ids=meta.get("knowledge_base_ids") + if isinstance(meta.get("knowledge_base_ids"), list) + else None, + locale=locale, + ) if mcp_servers: request["mcp_servers"] = mcp_servers @@ -745,6 +776,31 @@ async def _build_dashboard_request( request["configurable"] = configurable return request + def _attach_turn_knowledge_config( + self, + request: dict[str, Any], + *, + user_id: int, + is_admin: bool, + explicit_ids: list[str] | None, + locale: str, + ) -> None: + """Expose selected knowledge-base ids for the search_knowledge tool.""" + if self._knowledge_services is None: + return + bases = ( + self._knowledge_services.knowledge_repo.list_all() + if is_admin + else self._knowledge_services.knowledge_repo.list_visible(user_id) + ) + selected_ids = merge_knowledge_base_ids(bases, explicit_ids, owner_user_id=user_id) + configurable = dict(request.get("configurable") or {}) + configurable["knowledge_base_ids"] = selected_ids + configurable["knowledge_base_catalog"] = catalog_for_selected_bases(bases, selected_ids) + configurable["user_is_admin"] = is_admin + configurable["locale"] = locale + request["configurable"] = configurable + async def _resolve_turn_mcp_servers( self, *, diff --git a/src/octop/infra/gateway/process/response_mode.py b/src/octop/infra/gateway/process/response_mode.py new file mode 100644 index 00000000..93cc79a0 --- /dev/null +++ b/src/octop/infra/gateway/process/response_mode.py @@ -0,0 +1,118 @@ +"""Channel response delivery modes for external IM transports.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from typing import Any, Literal + +from harness_gateway.channel import MessageProcessor +from harness_gateway.models import ( + ContentPart, + InboundMessage, + MessageEvent, + MessageEventType, + TextContent, +) + +ChannelResponseMode = Literal["invoke", "stream"] + +DEFAULT_CHANNEL_RESPONSE_MODE: ChannelResponseMode = "invoke" + + +def normalize_channel_response_mode(value: Any) -> ChannelResponseMode: + """Return a supported response mode, defaulting external IM to one-shot delivery.""" + if isinstance(value, str) and value.strip().lower() == "stream": + return "stream" + return DEFAULT_CHANNEL_RESPONSE_MODE + + +async def collapse_to_invoke_response( + events: AsyncIterator[MessageEvent], +) -> AsyncIterator[MessageEvent]: + """Collapse a streamed agent turn into one final user-visible response. + + The agent still runs through the streaming path, preserving cancellation, + usage tracking, HITL, and tool media extraction. Text emitted before a tool + call is treated as progress narration and discarded. The remaining final + text and any generated media are delivered together when the turn completes. + """ + text_buffer = "" + media_buffer: list[ContentPart] = [] + + async for event in events: + if event.type == MessageEventType.DELTA: + for part in event.content: + if isinstance(part, TextContent) and part.text: + text_buffer += part.text + continue + + if event.type == MessageEventType.MESSAGE: + for part in event.content: + if isinstance(part, TextContent): + text = part.text.strip() + if text: + if text_buffer and not text_buffer.endswith("\n"): + text_buffer += "\n" + text_buffer += text + else: + media_buffer.append(part) + continue + + if event.type == MessageEventType.TOOL_START: + # Assistant text immediately followed by a tool call is progress + # narration, not the final answer shown to an IM user. + text_buffer = "" + continue + + if event.type in ( + MessageEventType.TYPING, + MessageEventType.THINKING, + MessageEventType.THINKING_DELTA, + MessageEventType.FLUSH, + MessageEventType.TOOL_END, + ): + continue + + if event.type == MessageEventType.ERROR: + text_buffer = "" + media_buffer.clear() + yield event + continue + + if event.type == MessageEventType.COMPLETED: + content: list[ContentPart] = [] + final_text = text_buffer.strip() + if final_text: + content.append(TextContent(text=final_text)) + content.extend(media_buffer) + if content: + yield MessageEvent(type=MessageEventType.MESSAGE, content=content) + yield event + return + + # Preserve future terminal/control events instead of silently losing them. + yield event + + +def processor_for_response_mode( + processor: MessageProcessor, + mode: ChannelResponseMode, +) -> MessageProcessor: + """Wrap *processor* with invoke-style delivery when requested.""" + if mode == "stream": + return processor + + async def _invoke_processor(message: InboundMessage) -> AsyncIterator[MessageEvent]: + async for event in collapse_to_invoke_response(processor(message)): + yield event + + return _invoke_processor + + +__all__ = [ + "ChannelResponseMode", + "DEFAULT_CHANNEL_RESPONSE_MODE", + "collapse_to_invoke_response", + "normalize_channel_response_mode", + "processor_for_response_mode", +] diff --git a/src/octop/infra/gateway/slash/ctx.py b/src/octop/infra/gateway/slash/ctx.py index 0017b4cd..9a19c9a3 100644 --- a/src/octop/infra/gateway/slash/ctx.py +++ b/src/octop/infra/gateway/slash/ctx.py @@ -56,8 +56,10 @@ def chat_type(ctx: SlashCtx) -> str: return parts[3] if len(parts) >= 4 else "dm" -def find_thread_by_short(registry: ThreadRegistry, agent_id: str, short: str) -> Any | None: - rows = registry.list_threads(agent_id=agent_id, limit=200) +def find_thread_by_short( + registry: ThreadRegistry, agent_id: str, user_id: int, short: str +) -> Any | None: + rows = registry.list_threads(agent_id=agent_id, user_id=user_id, limit=200) matches = [r for r in rows if r.thread_id.endswith(short)] return matches[0] if matches else None diff --git a/src/octop/infra/gateway/slash/handlers/session.py b/src/octop/infra/gateway/slash/handlers/session.py index 0eb03d92..2009f4a4 100644 --- a/src/octop/infra/gateway/slash/handlers/session.py +++ b/src/octop/infra/gateway/slash/handlers/session.py @@ -41,7 +41,7 @@ async def cmd_new(d: SlashDispatcher, cmd: SlashCommand, ctx: SlashCtx, sink: Sl async def cmd_list(d: SlashDispatcher, cmd: SlashCommand, ctx: SlashCtx, sink: SlashSink) -> None: lang = lang_of(ctx) - rows = ctx.thread_registry.list_threads(agent_id=ctx.agent_id, limit=10) + rows = ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id, limit=10) if not rows: await sink.text(tr("list.empty", lang)) return @@ -65,7 +65,7 @@ async def cmd_switch(d: SlashDispatcher, cmd: SlashCommand, ctx: SlashCtx, sink: if not short: await sink.text(tr("switch.usage", lang)) return - target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, short) + target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, ctx.user_id, short) if target is None: await sink.text(tr("switch.not_found", lang, short=short)) return @@ -81,7 +81,7 @@ async def cmd_resume(d: SlashDispatcher, cmd: SlashCommand, ctx: SlashCtx, sink: lang = lang_of(ctx) short = cmd.args.strip() if short: - target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, short) + target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, ctx.user_id, short) if target is None: await sink.text(tr("switch.not_found", lang, short=short)) return @@ -127,7 +127,7 @@ async def cmd_delete(d: SlashDispatcher, cmd: SlashCommand, ctx: SlashCtx, sink: await sink.text(tr("delete.usage", lang)) return active = ctx.thread_registry.get_bound_thread_id(ctx.session_key) - target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, short) + target = find_thread_by_short(ctx.thread_registry, ctx.agent_id, ctx.user_id, short) if target is None: await sink.text(tr("delete.not_found", lang, short=short)) return diff --git a/src/octop/infra/gateway/threads.py b/src/octop/infra/gateway/threads.py index c364707a..2c006e61 100644 --- a/src/octop/infra/gateway/threads.py +++ b/src/octop/infra/gateway/threads.py @@ -346,12 +346,36 @@ def touch_last_active(self, thread_id: str) -> None: def get_thread(self, thread_id: str) -> ThreadRow | None: return self._threads.get(thread_id) - def list_threads(self, *, agent_id: str, limit: int = 50) -> list[ThreadRow]: - return self._threads.list_by_agent(agent_id=agent_id, limit=limit) + def list_threads(self, *, agent_id: str, user_id: int, limit: int = 50) -> list[ThreadRow]: + return self._threads.list_by_agent_user(agent_id=agent_id, user_id=user_id, limit=limit) def list_threads_for_session(self, *, session_key: str, limit: int = 50) -> list[ThreadRow]: return self._threads.list_by_session(session_key=session_key, limit=limit) + def create_thread( + self, + *, + agent_id: str, + user_id: int, + channel_type: str, + session_key: str, + title: str | None = None, + last_active: int = 0, + thread_id: str | None = None, + ) -> str: + """Insert a thread row without rebinding the active session.""" + tid = thread_id or _new_thread_id() + self._threads.insert( + thread_id=tid, + agent_id=agent_id, + user_id=user_id, + channel_type=channel_type, + session_key=session_key, + title=title, + last_active=last_active, + ) + return tid + def delete_thread(self, thread_id: str) -> None: self._threads.delete(thread_id) diff --git a/src/octop/infra/knowledge/__init__.py b/src/octop/infra/knowledge/__init__.py new file mode 100644 index 00000000..c5f876f6 --- /dev/null +++ b/src/octop/infra/knowledge/__init__.py @@ -0,0 +1 @@ +"""Knowledge-base domain services.""" diff --git a/src/octop/infra/knowledge/chunk.py b/src/octop/infra/knowledge/chunk.py new file mode 100644 index 00000000..7fb12dee --- /dev/null +++ b/src/octop/infra/knowledge/chunk.py @@ -0,0 +1,21 @@ +"""Fixed-size, overlapping character chunking for knowledge documents.""" + +from __future__ import annotations + + +def chunk_text(text: str, *, size: int = 800, overlap: int = 120) -> list[str]: + """Split non-empty text into stable overlapping character windows.""" + if size <= 0: + raise ValueError("size must be positive") + if overlap < 0 or overlap >= size: + raise ValueError("overlap must be non-negative and smaller than size") + content = text.strip() + if not content: + return [] + step = size - overlap + chunks: list[str] = [] + for start in range(0, len(content), step): + chunks.append(content[start : start + size]) + if start + size >= len(content): + break + return chunks diff --git a/src/octop/infra/knowledge/default_open.py b/src/octop/infra/knowledge/default_open.py new file mode 100644 index 00000000..5d54164f --- /dev/null +++ b/src/octop/infra/knowledge/default_open.py @@ -0,0 +1,32 @@ +"""Knowledge-base selection defaults for a single chat turn.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Protocol + + +class _KnowledgeBase(Protocol): + id: str + owner_user_id: int + default_open: bool + + +def merge_knowledge_base_ids( + visible_bases: Sequence[_KnowledgeBase], + explicit_ids: list[str] | None, + *, + owner_user_id: int, +) -> list[str]: + """Use the actor's own default-open bases when a turn omits a list. + + ``default_open`` is per-owner preference: shared bases marked default-open + are auto-injected only for the creating user, not for other viewers. + """ + if explicit_ids is not None: + return list(explicit_ids) + return [ + base.id + for base in visible_bases + if base.default_open and int(base.owner_user_id) == int(owner_user_id) + ] diff --git a/src/octop/infra/knowledge/embed.py b/src/octop/infra/knowledge/embed.py new file mode 100644 index 00000000..d1b786ec --- /dev/null +++ b/src/octop/infra/knowledge/embed.py @@ -0,0 +1,39 @@ +"""Route knowledge-base embeddings to local ONNX or a configured provider.""" + +from __future__ import annotations + +from typing import Any + +import httpx + +from octop.infra.agents.providers.onnx_service import embed_texts +from octop.infra.agents.providers.probe import provider_headers + + +def embed_knowledge_texts(services: Any, texts: list[str]) -> list[list[float]]: + """Embed texts through the selected knowledge embedding backend.""" + settings = services.settings_repo + backend = (settings.get("knowledge_embedding_backend") or "onnx").strip().lower() + model = (settings.get("knowledge_embedding_model") or "").strip() + if backend != "remote": + return embed_texts(model, texts) + + provider_id = (settings.get("knowledge_embedding_provider_id") or "").strip() + provider = services.provider_repo.get(int(provider_id)) if provider_id.isdigit() else None + if provider is None or not provider.base_url or not provider.api_key: + raise RuntimeError("knowledge remote embedding provider is not ready") + headers: dict[str, str] = {"Authorization": f"Bearer {provider.api_key}"} + extra = provider_headers(provider) + if extra: + headers.update(extra) + with httpx.Client(timeout=60.0) as client: + response = client.post( + f"{provider.base_url.rstrip('/')}/embeddings", + headers=headers, + json={"model": model, "input": texts}, + ) + response.raise_for_status() + data = response.json().get("data") + if not isinstance(data, list): + raise RuntimeError("knowledge embedding response has no data") + return [list(item["embedding"]) for item in data] diff --git a/src/octop/infra/knowledge/files.py b/src/octop/infra/knowledge/files.py new file mode 100644 index 00000000..2e049e28 --- /dev/null +++ b/src/octop/infra/knowledge/files.py @@ -0,0 +1,37 @@ +"""Filesystem layout and safe document persistence for knowledge bases.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +from octop.infra.utils.paths import PathLayout + + +def knowledge_base_dir(kb_id: str) -> Path: + return PathLayout.from_env().knowledge_dir / kb_id + + +def documents_dir(kb_id: str) -> Path: + path = knowledge_base_dir(kb_id) / "docs" + path.mkdir(parents=True, exist_ok=True) + return path + + +def document_path(kb_id: str, doc_id: str, filename: str) -> Path: + suffix = Path(filename).suffix.lower() + return documents_dir(kb_id) / f"{doc_id}{suffix}" + + +def write_document(kb_id: str, doc_id: str, filename: str, content: bytes) -> Path: + path = document_path(kb_id, doc_id, filename) + path.write_bytes(content) + return path + + +def delete_document_file(kb_id: str, doc_id: str, filename: str) -> None: + document_path(kb_id, doc_id, filename).unlink(missing_ok=True) + + +def delete_knowledge_base_files(kb_id: str) -> None: + shutil.rmtree(knowledge_base_dir(kb_id), ignore_errors=True) diff --git a/src/octop/infra/knowledge/gate.py b/src/octop/infra/knowledge/gate.py new file mode 100644 index 00000000..5eb4fc7e --- /dev/null +++ b/src/octop/infra/knowledge/gate.py @@ -0,0 +1,142 @@ +"""Capability gate for the optional knowledge-base feature.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from typing import Any + +from octop.infra.agents.providers.model_flags import is_embedding_model +from octop.infra.agents.providers.onnx_service import ( + embedding_prerequisites_ok_for_model, + is_model_downloaded, + local_embedding_deps_available, + require_embedding_prerequisites_for_model, +) + +_FEATURE_ENABLED_KEY = "knowledge_bases_enabled" +_EMBEDDING_BACKEND_KEY = "knowledge_embedding_backend" +_EMBEDDING_MODEL_KEY = "knowledge_embedding_model" +_EMBEDDING_PROVIDER_ID_KEY = "knowledge_embedding_provider_id" + +SettingsGet = Callable[[str], str | None] +SettingsSet = Callable[[str, str], None] + + +def _as_bool(value: str | None) -> bool: + if value is None: + return False + try: + decoded: Any = json.loads(value) + except json.JSONDecodeError: + decoded = value + if isinstance(decoded, bool): + return decoded + return str(decoded).strip().lower() == "true" + + +def _provider_for_settings(provider_repo: Any, provider_id: str) -> Any | None: + if provider_repo is None or not provider_id.isdigit(): + return None + return provider_repo.get(int(provider_id)) + + +def _remote_ready(provider: Any | None, model: str) -> bool: + return bool( + provider + and provider.enabled + and provider.api_key + and provider.base_url + and any( + str(candidate.get("id") or "").strip() == model + and is_embedding_model( + candidate, + provider_name=provider.name, + provider_api_key=getattr(provider, "api_key", None), + ) + for candidate in provider.get_models() + ) + ) + + +def get_capability(settings_get: SettingsGet, provider_repo: Any = None) -> dict[str, Any]: + """Return feature state and its independent embedding readiness checks.""" + selected_model = (settings_get(_EMBEDDING_MODEL_KEY) or "").strip() + backend = (settings_get(_EMBEDDING_BACKEND_KEY) or "onnx").strip().lower() + backend = backend if backend in {"onnx", "remote"} else "onnx" + provider_id = (settings_get(_EMBEDDING_PROVIDER_ID_KEY) or "").strip() + provider = _provider_for_settings(provider_repo, provider_id) + deps_available = local_embedding_deps_available() + model_downloaded = bool(selected_model and is_model_downloaded(selected_model)) + provider_ready = _remote_ready(provider, selected_model) + prerequisites_ok = ( + provider_ready + if backend == "remote" + else embedding_prerequisites_ok_for_model(selected_model) + ) + feature_enabled = _as_bool(settings_get(_FEATURE_ENABLED_KEY)) + return { + "feature_enabled": feature_enabled, + "selected_model": selected_model, + "backend": backend, + "provider_id": provider_id, + "prerequisites_ok": prerequisites_ok, + "usable": feature_enabled and prerequisites_ok, + "checks": { + "model_selected": bool(selected_model), + "model_downloaded": model_downloaded, + "deps_available": deps_available, + "provider_ready": provider_ready, + }, + } + + +def set_feature_enabled( + settings_get: SettingsGet, + settings_set: SettingsSet, + *, + enabled: bool, + backend: str | None = None, + model: str | None, + provider_id: str | None = None, + provider_repo: Any = None, +) -> None: + """Enable only after model-specific prerequisites pass.""" + if not enabled: + settings_set(_FEATURE_ENABLED_KEY, "false") + if model is not None: + settings_set(_EMBEDDING_MODEL_KEY, model.strip()) + if backend is not None: + settings_set(_EMBEDDING_BACKEND_KEY, backend) + if provider_id is not None: + settings_set(_EMBEDDING_PROVIDER_ID_KEY, provider_id.strip()) + return + selected_backend = (backend or "onnx").strip().lower() + if selected_backend not in {"onnx", "remote"}: + raise ValueError("knowledge embedding backend must be onnx or remote") + selected_model = (model or "").strip() + if not selected_model: + raise ValueError("enabling knowledge bases requires an embedding model") + if selected_backend == "remote": + selected_provider_id = (provider_id or "").strip() + if not _remote_ready( + _provider_for_settings(provider_repo, selected_provider_id), selected_model + ): + raise ValueError("knowledge remote embedding provider is not ready") + verified_model = selected_model + else: + selected_provider_id = "" + verified_model = require_embedding_prerequisites_for_model(selected_model) + settings_set(_EMBEDDING_BACKEND_KEY, selected_backend) + settings_set(_EMBEDDING_MODEL_KEY, verified_model) + settings_set(_EMBEDDING_PROVIDER_ID_KEY, selected_provider_id) + settings_set(_FEATURE_ENABLED_KEY, "true") + + +def assert_knowledge_usable(settings_get: SettingsGet, provider_repo: Any = None) -> None: + """Raise a distinguishable runtime error when the capability cannot be used.""" + capability = get_capability(settings_get, provider_repo) + if not capability["feature_enabled"]: + raise RuntimeError("knowledge feature is disabled") + if not capability["prerequisites_ok"]: + raise RuntimeError("knowledge embedding prerequisites are not satisfied") diff --git a/src/octop/infra/knowledge/hint.py b/src/octop/infra/knowledge/hint.py new file mode 100644 index 00000000..2598601c --- /dev/null +++ b/src/octop/infra/knowledge/hint.py @@ -0,0 +1,119 @@ +"""Per-turn enrichment of the search_knowledge tool description.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Mapping, Sequence +from typing import Any + +from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse +from langchain_core.tools.base import BaseTool +from langgraph.config import get_config + +from octop.infra.knowledge.tools import SEARCH_KNOWLEDGE_TOOL, format_search_knowledge_description + + +def catalog_for_selected_bases( + bases: Sequence[Any], + selected_ids: Sequence[str], +) -> list[dict[str, str]]: + """Build ``{id, name, description}`` rows for visible selected bases.""" + by_id = {str(getattr(base, "id", "")): base for base in bases} + catalog: list[dict[str, str]] = [] + for kb_id in selected_ids: + key = str(kb_id).strip() + if not key: + continue + base = by_id.get(key) + if base is None: + continue + raw_name = getattr(base, "name", "") + name = raw_name.strip() if isinstance(raw_name, str) else "" + raw_desc = getattr(base, "description", "") + description = raw_desc.strip() if isinstance(raw_desc, str) else "" + catalog.append( + { + "id": key, + "name": name or key, + "description": description, + } + ) + return catalog + + +def _catalog_from_config() -> list[dict[str, str]]: + cfg = get_config().get("configurable") or {} + raw = cfg.get("knowledge_base_catalog") + if not isinstance(raw, list): + return [] + out: list[dict[str, str]] = [] + for item in raw: + if not isinstance(item, Mapping): + continue + kb_id = str(item.get("id") or "").strip() + name = str(item.get("name") or "").strip() or kb_id + description = str(item.get("description") or "").strip() + if not name: + continue + out.append({"id": kb_id or name, "name": name, "description": description}) + return out + + +def _with_enriched_tool_description(request: ModelRequest[Any]) -> ModelRequest[Any]: + catalog = _catalog_from_config() + tools_in = list(request.tools or []) + if not tools_in: + return request + + if not catalog: + filtered = [ + tool + for tool in tools_in + if not (isinstance(tool, BaseTool) and tool.name == SEARCH_KNOWLEDGE_TOOL) + ] + if len(filtered) == len(tools_in): + return request + return request.override(tools=filtered) + + description = format_search_knowledge_description(catalog) + tools_out: list[Any] = [] + changed = False + for tool in tools_in: + if isinstance(tool, BaseTool) and tool.name == SEARCH_KNOWLEDGE_TOOL: + if tool.description == description: + tools_out.append(tool) + else: + tools_out.append(tool.model_copy(update={"description": description})) + changed = True + continue + tools_out.append(tool) + if not changed: + return request + return request.override(tools=tools_out) + + +class KnowledgeSearchHintMiddleware(AgentMiddleware[Any, Any]): + """Rewrite ``search_knowledge`` description with this turn's KB titles/descriptions. + + Hide the tool entirely when nothing is attached — a "do not call" note in + the schema is weaker than omitting the tool from the model's tool list. + """ + + def wrap_model_call( + self, + request: ModelRequest[Any], + handler: Callable[[ModelRequest[Any]], ModelResponse[Any]], + ) -> ModelResponse[Any]: + return handler(_with_enriched_tool_description(request)) + + async def awrap_model_call( + self, + request: ModelRequest[Any], + handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]], + ) -> ModelResponse[Any]: + return await handler(_with_enriched_tool_description(request)) + + +__all__ = [ + "KnowledgeSearchHintMiddleware", + "catalog_for_selected_bases", +] diff --git a/src/octop/infra/knowledge/index.py b/src/octop/infra/knowledge/index.py new file mode 100644 index 00000000..74c9867b --- /dev/null +++ b/src/octop/infra/knowledge/index.py @@ -0,0 +1,134 @@ +"""Per-knowledge-base SQLite sidecar vector index.""" + +from __future__ import annotations + +import json +import math +import sqlite3 +import struct +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path + +from octop.infra.utils.paths import PathLayout + + +@dataclass(frozen=True) +class Hit: + chunk_id: str + doc_id: str + ordinal: int + text: str + score: float + metadata: dict[str, object] + + +class KnowledgeIndex: + """Store chunk embeddings in one local SQLite database per knowledge base.""" + + def __init__(self, kb_id: str) -> None: + self._path = PathLayout.from_env().knowledge_dir / kb_id / "index.sqlite" + self._initialize() + + @property + def path(self) -> Path: + return self._path + + def _connect(self) -> sqlite3.Connection: + return sqlite3.connect(self._path) + + def _initialize(self) -> None: + self._path.parent.mkdir(parents=True, exist_ok=True) + with self._connect() as conn: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS chunks ( + chunk_id TEXT PRIMARY KEY, + doc_id TEXT NOT NULL, + ordinal INTEGER NOT NULL, + text TEXT NOT NULL, + embedding BLOB NOT NULL, + meta_json TEXT NOT NULL DEFAULT '{}' + ); + CREATE INDEX IF NOT EXISTS idx_chunks_doc ON chunks(doc_id); + """ + ) + + def replace_doc_chunks( + self, + doc_id: str, + texts: Sequence[str], + embeddings: Sequence[Sequence[float]], + *, + metadata: Sequence[dict[str, object]] | None = None, + ) -> None: + """Atomically replace all chunks belonging to one document.""" + if len(texts) != len(embeddings): + raise ValueError("texts and embeddings must have the same length") + if metadata is not None and len(metadata) != len(texts): + raise ValueError("metadata and texts must have the same length") + rows: list[tuple[str, str, int, str, bytes, str]] = [] + for ordinal, (text, embedding) in enumerate(zip(texts, embeddings, strict=True)): + vector = [float(value) for value in embedding] + if not vector: + raise ValueError("embedding cannot be empty") + meta = metadata[ordinal] if metadata is not None else {} + rows.append( + ( + f"{doc_id}:{ordinal}", + doc_id, + ordinal, + text, + struct.pack(f"<{len(vector)}f", *vector), + json.dumps(meta, ensure_ascii=False, separators=(",", ":")), + ) + ) + with self._connect() as conn: + conn.execute("DELETE FROM chunks WHERE doc_id = ?", (doc_id,)) + conn.executemany( + "INSERT INTO chunks(chunk_id, doc_id, ordinal, text, embedding, meta_json) " + "VALUES (?, ?, ?, ?, ?, ?)", + rows, + ) + + def delete_doc(self, doc_id: str) -> None: + with self._connect() as conn: + conn.execute("DELETE FROM chunks WHERE doc_id = ?", (doc_id,)) + + def search(self, query_vec: Sequence[float], k: int) -> list[Hit]: + """Return the ``k`` best chunk hits using in-process cosine similarity.""" + if k <= 0: + return [] + query = [float(value) for value in query_vec] + if not query: + raise ValueError("query vector cannot be empty") + query_norm = math.sqrt(sum(value * value for value in query)) + if query_norm == 0: + raise ValueError("query vector cannot be zero") + with self._connect() as conn: + rows = conn.execute( + "SELECT chunk_id, doc_id, ordinal, text, embedding, meta_json FROM chunks" + ).fetchall() + hits: list[Hit] = [] + for chunk_id, doc_id, ordinal, text, blob, meta_json in rows: + embedding = struct.unpack(f"<{len(blob) // 4}f", blob) + if len(embedding) != len(query): + continue + norm = math.sqrt(sum(value * value for value in embedding)) + score = ( + 0.0 + if norm == 0 + else sum(a * b for a, b in zip(query, embedding, strict=True)) / (query_norm * norm) + ) + decoded = json.loads(meta_json) + hits.append( + Hit( + chunk_id=chunk_id, + doc_id=doc_id, + ordinal=ordinal, + text=text, + score=score, + metadata=decoded if isinstance(decoded, dict) else {}, + ) + ) + return sorted(hits, key=lambda hit: hit.score, reverse=True)[:k] diff --git a/src/octop/infra/knowledge/jobs.py b/src/octop/infra/knowledge/jobs.py new file mode 100644 index 00000000..1a3f162d --- /dev/null +++ b/src/octop/infra/knowledge/jobs.py @@ -0,0 +1,80 @@ +"""In-process document indexing jobs.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from octop.infra.knowledge.chunk import chunk_text +from octop.infra.knowledge.embed import embed_knowledge_texts +from octop.infra.knowledge.files import document_path +from octop.infra.knowledge.gate import assert_knowledge_usable +from octop.infra.knowledge.index import KnowledgeIndex +from octop.infra.knowledge.parse import parse_document + +INDEX_CONCURRENCY = 2 +_index_semaphore: asyncio.Semaphore | None = None + + +def reset_index_semaphore_for_tests() -> None: + """Drop the cached semaphore so tests can change INDEX_CONCURRENCY.""" + global _index_semaphore + _index_semaphore = None + + +def _get_index_semaphore() -> asyncio.Semaphore: + global _index_semaphore + if _index_semaphore is None: + _index_semaphore = asyncio.Semaphore(INDEX_CONCURRENCY) + return _index_semaphore + + +def process_document(services: Any, kb_id: str, doc_id: str) -> None: + """Synchronously parse, embed, and atomically replace one document's chunks.""" + repo = services.knowledge_repo + assert_knowledge_usable(services.settings_repo.get, getattr(services, "provider_repo", None)) + document = repo.get_document(doc_id) + base = repo.get_base(kb_id) + if document is None or document.kb_id != kb_id or base is None: + raise LookupError("knowledge document or base not found") + repo.update_document(doc_id, status="processing", error_message="") + try: + text = parse_document(document_path(kb_id, doc_id, document.filename)) + chunks = chunk_text(text) + if not (text or "").strip() or not chunks: + raise ValueError("knowledge document has no extractable text") + embeddings = embed_knowledge_texts(services, chunks) + KnowledgeIndex(kb_id).replace_doc_chunks(doc_id, chunks, embeddings) + dimension = len(embeddings[0]) if embeddings else 0 + repo.update_document(doc_id, status="ready", error_message="", chunk_count=len(chunks)) + if dimension and base.embedding_dim != dimension: + repo.update_base(kb_id, embedding_dim=dimension) + except Exception as exc: + repo.update_document(doc_id, status="failed", error_message=str(exc), chunk_count=0) + raise + + +def enqueue_index_document(services: Any, kb_id: str, doc_id: str) -> asyncio.Task[None]: + """Schedule CPU- and I/O-bound indexing outside the event loop.""" + loop = asyncio.get_running_loop() + sem = _get_index_semaphore() + + async def _run() -> None: + async with sem: + await loop.run_in_executor(None, process_document, services, kb_id, doc_id) + + return asyncio.create_task(_run()) + + +def reindex_all_documents(services: Any, embedding_model: str) -> None: + """Reset every index to use ``embedding_model`` and schedule fresh work.""" + documents = services.knowledge_repo.reindex_all_documents(embedding_model) + for document in documents: + enqueue_index_document(services, document.kb_id, document.id) + + +def resume_pending_index_jobs(services: Any) -> None: + """Resume pending work and jobs interrupted by a prior process shutdown.""" + documents = services.knowledge_repo.resume_pending_documents() + for document in documents: + enqueue_index_document(services, document.kb_id, document.id) diff --git a/src/octop/infra/knowledge/parse.py b/src/octop/infra/knowledge/parse.py new file mode 100644 index 00000000..f1eb5b33 --- /dev/null +++ b/src/octop/infra/knowledge/parse.py @@ -0,0 +1,30 @@ +"""Text extraction for the document types accepted by knowledge bases.""" + +from __future__ import annotations + +from pathlib import Path + + +def parse_document(path: Path) -> str: + """Extract searchable text from a supported local document.""" + suffix = path.suffix.lower() + if suffix in {".md", ".txt"}: + return path.read_text(encoding="utf-8", errors="replace") + if suffix == ".pdf": + from pypdf import PdfReader + + return "\n".join(page.extract_text() or "" for page in PdfReader(path).pages) + if suffix == ".docx": + from docx import Document + + return "\n".join(paragraph.text for paragraph in Document(str(path)).paragraphs) + if suffix == ".pptx": + from pptx import Presentation + + return "\n".join( + shape.text + for slide in Presentation(str(path)).slides + for shape in slide.shapes + if hasattr(shape, "text") and shape.text + ) + raise ValueError(f"unsupported knowledge document extension: {suffix or '(none)'}") diff --git a/src/octop/infra/knowledge/retrieve.py b/src/octop/infra/knowledge/retrieve.py new file mode 100644 index 00000000..bb264533 --- /dev/null +++ b/src/octop/infra/knowledge/retrieve.py @@ -0,0 +1,130 @@ +"""Retrieve readable knowledge-base chunks for one chat turn.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Sequence +from typing import Any + +from octop.i18n import tr +from octop.infra.knowledge.embed import embed_knowledge_texts +from octop.infra.knowledge.gate import assert_knowledge_usable +from octop.infra.knowledge.index import Hit, KnowledgeIndex + +DEFAULT_RETRIEVAL_K = 8 +DEFAULT_CONTEXT_CHAR_BUDGET = 6000 + +logger = logging.getLogger(__name__) + + +async def retrieve_context( + services: Any, + *, + user_id: int, + is_admin: bool, + query: str | None, + knowledge_base_ids: Sequence[str], + k: int = DEFAULT_RETRIEVAL_K, + char_budget: int = DEFAULT_CONTEXT_CHAR_BUDGET, + locale: str = "en", + visible_bases: Sequence[Any] | None = None, +) -> str: + """Return formatted readable context, skipping unavailable or failed retrieval.""" + if not (query or "").strip() or not knowledge_base_ids or k <= 0 or char_budget <= 0: + return "" + loop = asyncio.get_running_loop() + try: + return await loop.run_in_executor( + None, + lambda: _retrieve_context_sync( + services, + user_id=user_id, + is_admin=is_admin, + query=query, + knowledge_base_ids=knowledge_base_ids, + k=k, + char_budget=char_budget, + locale=locale, + visible_bases=visible_bases, + ), + ) + except Exception: + logger.warning("knowledge retrieval skipped for user=%s", user_id, exc_info=True) + return "" + + +def _retrieve_context_sync( + services: Any, + *, + user_id: int, + is_admin: bool, + query: str | None, + knowledge_base_ids: Sequence[str], + k: int, + char_budget: int, + locale: str, + visible_bases: Sequence[Any] | None, +) -> str: + assert_knowledge_usable(services.settings_repo.get, getattr(services, "provider_repo", None)) + query_vectors = embed_knowledge_texts(services, [(query or "").strip()]) + if not query_vectors: + return "" + + visible = visible_bases + if visible is None: + visible = ( + services.knowledge_repo.list_all() + if is_admin + else services.knowledge_repo.list_visible(user_id) + ) + visible_by_id = {base.id: base for base in visible} + selected_ids = _unique_ids(knowledge_base_ids) + + ranked: list[tuple[Any, Hit, Any]] = [] + for kb_id in selected_ids: + base = visible_by_id.get(kb_id) + if base is None: + continue + ready_documents = { + document.id: document + for document in services.knowledge_repo.list_documents(kb_id) + if document.status == "ready" + } + for hit in KnowledgeIndex(kb_id).search(query_vectors[0], k=k): + document = ready_documents.get(hit.doc_id) + if document is not None: + ranked.append((base, hit, document)) + + ranked.sort(key=lambda item: item[1].score, reverse=True) + return _format_context(ranked[:k], char_budget=char_budget, locale=locale) + + +def _unique_ids(ids: Sequence[str]) -> list[str]: + return list(dict.fromkeys(str(kb_id).strip() for kb_id in ids if str(kb_id).strip())) + + +def _format_context( + ranked: Sequence[tuple[Any, Hit, Any]], *, char_budget: int, locale: str +) -> str: + sections: list[str] = [] + remaining = char_budget + for base, hit, document in ranked: + citation = tr( + "knowledge.retrieval.citation", + locale, + name=base.name, + filename=document.filename, + ordinal=hit.ordinal + 1, + ) + if remaining <= len(citation): + break + text = hit.text.strip()[: remaining - len(citation)] + if not text: + continue + section = f"{citation}{text}" + sections.append(section) + remaining -= len(section) + if not sections: + return "" + return tr("knowledge.retrieval.preamble", locale) + "\n\n" + "\n\n".join(sections) diff --git a/src/octop/infra/knowledge/service.py b/src/octop/infra/knowledge/service.py new file mode 100644 index 00000000..ce19434a --- /dev/null +++ b/src/octop/infra/knowledge/service.py @@ -0,0 +1,242 @@ +"""Knowledge-base ownership checks and document upload orchestration. + +Visibility is owner or instance-wide ``shared`` — no per-user ACL members. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, cast + +from octop.infra.db.repos.knowledge import KnowledgeBaseRow, KnowledgeDocumentRow +from octop.infra.knowledge.files import ( + delete_document_file, + delete_knowledge_base_files, + document_path, + write_document, +) +from octop.infra.knowledge.gate import assert_knowledge_usable +from octop.infra.knowledge.index import KnowledgeIndex +from octop.infra.knowledge.parse import parse_document + +MAX_DOCS_PER_KB = 100 +MAX_BASES_PER_OWNER = 20 +MAX_DOCUMENT_BYTES = 20 * 1024 * 1024 # 20 MiB +_MAX_PREVIEW_CHARS = 200_000 +_EXT_TO_CONTENT_TYPE = { + ".txt": "text/plain", + ".md": "text/markdown", + ".pdf": "application/pdf", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", +} +_ALLOWED_CONTENT_TYPES = set(_EXT_TO_CONTENT_TYPE.values()) + + +def _resolve_content_type(filename: str, content_type: str) -> str: + ct = (content_type or "").strip().lower() + if ct in _ALLOWED_CONTENT_TYPES: + return ct + if ct in {"", "application/octet-stream"}: + return _EXT_TO_CONTENT_TYPE.get(Path(filename).suffix.lower(), ct) + return ct + + +class KnowledgeService: + """Apply ownership while keeping control-plane rows and files synchronized.""" + + def __init__(self, services: Any) -> None: + self._services = services + + @property + def _repo(self) -> Any: + return self._services.knowledge_repo + + def create_base( + self, + *, + owner_user_id: int, + name: str, + description: str = "", + default_open: bool = False, + shared: bool = False, + icon_name: str = "", + ) -> KnowledgeBaseRow: + assert_knowledge_usable( + self._services.settings_repo.get, getattr(self._services, "provider_repo", None) + ) + owned = self._repo.count_bases_for_owner(owner_user_id) + if owned >= MAX_BASES_PER_OWNER: + raise ValueError(f"a user can own at most {MAX_BASES_PER_OWNER} knowledge bases") + model = (self._services.settings_repo.get("knowledge_embedding_model") or "").strip() + return cast( + KnowledgeBaseRow, + self._repo.create_base( + owner_user_id=owner_user_id, + name=name, + description=description, + default_open=default_open, + shared=shared, + icon_name=icon_name, + embedding_model=model, + ), + ) + + def list_visible_bases( + self, *, actor_user_id: int, is_admin: bool = False + ) -> list[KnowledgeBaseRow]: + if is_admin: + return cast(list[KnowledgeBaseRow], self._repo.list_all()) + return cast(list[KnowledgeBaseRow], self._repo.list_visible(actor_user_id)) + + def update_base( + self, + kb_id: str, + *, + actor_user_id: int, + name: str | None = None, + description: str | None = None, + default_open: bool | None = None, + shared: bool | None = None, + icon_name: str | None = None, + is_admin: bool = False, + ) -> KnowledgeBaseRow: + self.require_owner(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + self._repo.update_base( + kb_id, + name=name, + description=description, + default_open=default_open, + shared=shared, + icon_name=icon_name, + ) + return self._require_base(kb_id) + + def list_documents( + self, kb_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> list[KnowledgeDocumentRow]: + self.get_readable_base(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + return cast(list[KnowledgeDocumentRow], self._repo.list_documents(kb_id)) + + def preview_document( + self, kb_id: str, doc_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> dict[str, str]: + """Return extracted plain text for a readable knowledge document.""" + self.get_readable_base(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + document = self._repo.get_document(doc_id) + if document is None or document.kb_id != kb_id: + raise LookupError("knowledge document not found") + text = parse_document(document_path(kb_id, doc_id, document.filename)) + if len(text) > _MAX_PREVIEW_CHARS: + text = text[:_MAX_PREVIEW_CHARS] + return { + "id": document.id, + "filename": document.filename, + "text": text, + } + + def get_readable_base( + self, kb_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> KnowledgeBaseRow: + base = self._require_base(kb_id) + if is_admin or base.owner_user_id == actor_user_id or base.shared: + return base + raise PermissionError("knowledge base read access is required") + + def get_writable_base( + self, kb_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> KnowledgeBaseRow: + base = self._require_base(kb_id) + if is_admin or base.owner_user_id == actor_user_id: + return base + raise PermissionError("knowledge base write access is required") + + def require_owner( + self, kb_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> KnowledgeBaseRow: + base = self._require_base(kb_id) + if is_admin or base.owner_user_id == actor_user_id: + return base + raise PermissionError("knowledge base owner access is required") + + def upload_document( + self, + kb_id: str, + *, + actor_user_id: int, + filename: str, + content_type: str, + content: bytes, + is_admin: bool = False, + ) -> KnowledgeDocumentRow: + assert_knowledge_usable( + self._services.settings_repo.get, getattr(self._services, "provider_repo", None) + ) + self.get_writable_base(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + if len(content) > MAX_DOCUMENT_BYTES: + raise ValueError( + f"knowledge document size exceeds maximum of {MAX_DOCUMENT_BYTES} bytes" + ) + resolved_type = _resolve_content_type(filename, content_type) + if resolved_type not in _ALLOWED_CONTENT_TYPES: + raise ValueError(f"unsupported knowledge document content type: {content_type}") + if not filename or PathLikeName(filename).is_unsafe: + raise ValueError("invalid knowledge document filename") + document = self._repo.create_document( + kb_id=kb_id, + filename=filename, + content_type=resolved_type, + byte_size=len(content), + max_documents=MAX_DOCS_PER_KB, + ) + try: + write_document(kb_id, document.id, filename, content) + except Exception: + self._repo.delete_document(document.id) + raise + return cast(KnowledgeDocumentRow, document) + + def delete_document( + self, kb_id: str, doc_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> None: + self.get_writable_base(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + document = self._repo.get_document(doc_id) + if document is None or document.kb_id != kb_id: + raise LookupError("knowledge document not found") + KnowledgeIndex(kb_id).delete_doc(doc_id) + delete_document_file(kb_id, doc_id, document.filename) + self._repo.delete_document(doc_id) + + def reindex_document( + self, kb_id: str, doc_id: str, *, actor_user_id: int, is_admin: bool = False + ) -> KnowledgeDocumentRow: + assert_knowledge_usable( + self._services.settings_repo.get, getattr(self._services, "provider_repo", None) + ) + self.get_writable_base(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + document = self._repo.get_document(doc_id) + if document is None or document.kb_id != kb_id: + raise LookupError("knowledge document not found") + self._repo.update_document(doc_id, status="pending", error_message="", chunk_count=0) + refreshed = self._repo.get_document(doc_id) + if refreshed is None: + raise LookupError("knowledge document not found") + return cast(KnowledgeDocumentRow, refreshed) + + def delete_base(self, kb_id: str, *, actor_user_id: int, is_admin: bool = False) -> None: + self.require_owner(kb_id, actor_user_id=actor_user_id, is_admin=is_admin) + self._repo.delete_base(kb_id) + delete_knowledge_base_files(kb_id) + + def _require_base(self, kb_id: str) -> KnowledgeBaseRow: + base = self._repo.get_base(kb_id) + if base is None: + raise LookupError("knowledge base not found") + return cast(KnowledgeBaseRow, base) + + +class PathLikeName: + """Minimal filename safety check; document ids, not names, form the path.""" + + def __init__(self, value: str) -> None: + self.is_unsafe = not value.strip() or "/" in value or "\\" in value diff --git a/src/octop/infra/knowledge/tools.py b/src/octop/infra/knowledge/tools.py new file mode 100644 index 00000000..165c388c --- /dev/null +++ b/src/octop/infra/knowledge/tools.py @@ -0,0 +1,120 @@ +"""Built-in LangChain tool for on-demand knowledge-base retrieval.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Annotated, Any + +from langchain_core.tools import StructuredTool +from langgraph.config import get_config +from pydantic import Field + +from octop.infra.knowledge.retrieve import DEFAULT_RETRIEVAL_K, retrieve_context + +SEARCH_KNOWLEDGE_TOOL = "search_knowledge" + +# API allows 2000-char KB descriptions; keep the tool schema compact. +_MAX_CATALOG_DESC_CHARS = 240 + +_SEARCH_DESC_BASE = ( + "Search the knowledge bases listed below for relevant passages. " + "Use this tool when the user's question overlaps any of those topics. " + "Write `query` as a focused search string (key terms, names, synonyms), " + "not a chat reply. Do not invent citations; only use returned passages." +) + +_SEARCH_DESC_NONE = "No knowledge bases are attached this turn." + + +def _clip_catalog_text(text: str, limit: int = _MAX_CATALOG_DESC_CHARS) -> str: + stripped = text.strip() + if len(stripped) <= limit: + return stripped + return stripped[: limit - 3].rstrip() + "..." + + +def format_search_knowledge_description( + catalog: Sequence[Mapping[str, str]] | None, +) -> str: + """Build the LLM-facing tool description, including attached KB titles/descriptions.""" + entries: list[str] = [] + for item in catalog or (): + name = _clip_catalog_text(str(item.get("name") or item.get("id") or ""), 80) + if not name: + continue + description = _clip_catalog_text(str(item.get("description") or "")) + if description: + entries.append(f"- {name}: {description}") + else: + entries.append(f"- {name}") + if not entries: + return _SEARCH_DESC_NONE + listed = "\n".join(entries) + return f"{_SEARCH_DESC_BASE}\nAttached this turn:\n{listed}" + + +def _tool_ctx() -> tuple[int, bool, list[str], str]: + cfg = get_config().get("configurable") or {} + user_raw = cfg.get("user") + if user_raw is None: + raise ValueError("missing configurable.user") + user_id = int(user_raw) + is_admin = bool(cfg.get("user_is_admin")) + raw_ids = cfg.get("knowledge_base_ids") + ids: list[str] = [] + if isinstance(raw_ids, list): + ids = [str(item).strip() for item in raw_ids if str(item).strip()] + locale = str(cfg.get("locale") or "en") + return user_id, is_admin, ids, locale + + +def build_knowledge_tools(services: Any) -> list[StructuredTool]: + """Return built-in knowledge retrieval tools (wired via HarnessAgentConfig.tools).""" + + async def search_knowledge( + query: Annotated[ + str, + Field( + description=( + "Focused search string for the selected knowledge bases: " + "key terms, names, and synonyms from the user's question. " + "Not a chat reply." + ), + ), + ], + k: Annotated[ + int, + Field( + description="Max passages to return (default 8).", + ge=1, + le=20, + ), + ] = DEFAULT_RETRIEVAL_K, + ) -> str: + try: + user_id, is_admin, kb_ids, locale = _tool_ctx() + if not kb_ids: + return "No knowledge bases selected for this turn." + context = await retrieve_context( + services, + user_id=user_id, + is_admin=is_admin, + query=query, + knowledge_base_ids=kb_ids, + k=k, + locale=locale, + ) + if not context: + return "No relevant passages found." + return context + except Exception as exc: + return f"Knowledge search failed: {exc}" + + return [ + StructuredTool.from_function( + coroutine=search_knowledge, + name=SEARCH_KNOWLEDGE_TOOL, + # Static fallback; middleware hides or rewrites this per turn. + description=format_search_knowledge_description([]), + ) + ] diff --git a/src/octop/infra/server.py b/src/octop/infra/server.py index eb8fb5bc..f76ebc6b 100644 --- a/src/octop/infra/server.py +++ b/src/octop/infra/server.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from logging.handlers import TimedRotatingFileHandler from pathlib import Path +from typing import TYPE_CHECKING from octop.config import OctopConfig, load_config from octop.infra.agents.experts.catalog import ExpertCatalog, default_library_root @@ -28,6 +29,9 @@ from octop.infra.users.manager import UserManager from octop.infra.utils.paths import PathLayout +if TYPE_CHECKING: + from octop.infra.auth.sso.service import SsoService + logger = logging.getLogger(__name__) @@ -110,12 +114,28 @@ def __init__(self, home: Path | None = None) -> None: self.wizard_tokens = WizardTokenStore(ttl_seconds=300) self._started = False self._started_at: int | None = None + self._sso_service: SsoService | None = None # Backward compat: expose user_manager directly @property def user_manager(self) -> UserManager | None: return self.app_runtime.user_manager if self.app_runtime else None + @property + def sso_service(self) -> SsoService: + """Process-level SSO service so discovery/JWKS cache survives across requests.""" + from octop.infra.auth.sso.service import SsoService as SsoServiceCls # noqa: PLC0415 + + if self.services is None or self.user_manager is None: + raise RuntimeError("SSO service requires a started server with user manager") + if ( + self._sso_service is None + or getattr(self._sso_service, "_services", None) is not self.services + or getattr(self._sso_service, "_user_manager", None) is not self.user_manager + ): + self._sso_service = SsoServiceCls(self.services, self.user_manager) + return self._sso_service + @property def database_bound(self) -> bool: return self.services is not None and self.app_runtime is not None @@ -229,6 +249,10 @@ async def _boot_runtime(self, config: OctopConfig) -> None: install_auto_renewal_job(cron_mgr, paths=self.paths) + from octop.infra.backup.auto import install_auto_backup_job + + install_auto_backup_job(cron_mgr, server=self) + registry.set_cron_manager(cron_mgr) registry.set_team_processor(gateway.processor) @@ -258,6 +282,9 @@ async def _boot_runtime(self, config: OctopConfig) -> None: user_manager=user_mgr, proactive_scheduler=proactive_scheduler, ) + from octop.infra.knowledge.jobs import resume_pending_index_jobs # noqa: PLC0415 + + resume_pending_index_jobs(self.services) def _emit_wizard_password(self, *, user_count: int) -> None: config = self.config diff --git a/src/octop/infra/skills/skill_package_store.py b/src/octop/infra/skills/skill_package_store.py index 46770331..7ae1c8f3 100644 --- a/src/octop/infra/skills/skill_package_store.py +++ b/src/octop/infra/skills/skill_package_store.py @@ -22,7 +22,7 @@ ) from octop.infra.users.identity import User from octop.infra.utils.frontmatter import parse_frontmatter -from octop.infra.utils.ulid import new_ulid +from octop.infra.utils.ulid import new_short_id def is_skill_package_name_conflict(exc: BaseException) -> bool: @@ -66,6 +66,14 @@ def _apply_presentation_metadata(summary: dict[str, Any], metadata: dict[str, An summary["icon_url"] = icon_url +def _allocate_package_id(repo: SkillPackageRepo) -> str: + for _ in range(16): + package_id = new_short_id() + if repo.get(package_id) is None: + return package_id + raise RuntimeError("failed to allocate unique skill package id") + + class SkillPackageStore: """Persist skill package contents below a host-local root.""" @@ -83,7 +91,7 @@ def create( icon_url: str = "", ) -> SkillPackageRow: row = self.repo.create( - id=new_ulid(), + id=_allocate_package_id(self.repo), name=name, description=description, created_by=created_by, diff --git a/src/octop/infra/users/identity.py b/src/octop/infra/users/identity.py index 667d336f..d8ebf6fd 100644 --- a/src/octop/infra/users/identity.py +++ b/src/octop/infra/users/identity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import StrEnum @@ -18,6 +18,7 @@ class User: role: Role display_name: str | None locale: str = "zh" + permissions: list[str] = field(default_factory=list) @property def label(self) -> str: diff --git a/src/octop/infra/users/manager.py b/src/octop/infra/users/manager.py index 3a141a3a..d18edc63 100644 --- a/src/octop/infra/users/manager.py +++ b/src/octop/infra/users/manager.py @@ -4,16 +4,26 @@ import asyncio import builtins +import hashlib import logging +import re import shutil +import sqlite3 import time from typing import Any +try: + from psycopg import errors as pg_errors +except ImportError: # pragma: no cover - optional PostgreSQL driver + pg_errors = None # type: ignore[assignment] + from octop.infra.db.repos.audit import ACTOR_ADMIN +from octop.infra.db.repos.users import UserRepo from octop.infra.db.services import SharedServices from octop.infra.errors import ErrorCode, OctopError from octop.infra.users.identity import Role, User from octop.infra.users.password import hash_password, validate_password_policy, verify_password +from octop.infra.users.permissions import validate_permission_keys from octop.infra.users.preferences import ( ModelReasoningPreference, RemoteBrowserBookmark, @@ -25,6 +35,49 @@ from octop.infra.utils.locale import normalize_locale logger = logging.getLogger(__name__) +_USERNAME_ALLOWED = re.compile(r"[^a-zA-Z0-9_.-]") +_MAX_USERNAME_LENGTH = 64 + + +def allocate_username(repo: UserRepo, claims: dict[str, Any], subject: str) -> str: + """Return an available username derived from OIDC claims and subject.""" + preferred = claims.get("preferred_username") + email = claims.get("email") + candidate = preferred if isinstance(preferred, str) and preferred.strip() else None + if candidate is None and isinstance(email, str) and email.strip(): + candidate = email.strip().split("@", maxsplit=1)[0] + if candidate is None: + candidate = f"sso_{hashlib.sha256(subject.encode()).hexdigest()[:12]}" + + base = _USERNAME_ALLOWED.sub("", candidate)[:_MAX_USERNAME_LENGTH] + if not base: + base = f"sso_{hashlib.sha256(subject.encode()).hexdigest()[:12]}" + + username = base + suffix = 2 + while repo.get_by_username(username) is not None: + marker = f"_{suffix}" + username = f"{base[: _MAX_USERNAME_LENGTH - len(marker)]}{marker}" + suffix += 1 + return username + + +def _normalized_claim_email(claims: dict[str, Any]) -> str | None: + email = claims.get("email") + if not isinstance(email, str): + return None + return email.strip().lower() or None + + +def _claim_display_name(claims: dict[str, Any]) -> str | None: + name = claims.get("name") + return name.strip() if isinstance(name, str) and name.strip() else None + + +def _is_unique_violation(exc: BaseException) -> bool: + if isinstance(exc, sqlite3.IntegrityError): + return "unique" in str(exc).lower() + return pg_errors is not None and isinstance(exc, pg_errors.UniqueViolation) class UserManager: @@ -57,6 +110,7 @@ async def boot(self) -> None: role=Role(row.role), display_name=row.display_name, locale=normalize_locale(row.locale), + permissions=list(row.permissions), ) self._users[row.username] = user @@ -74,11 +128,16 @@ async def create( role: Role, display_name: str | None = None, locale: str | None = None, + permissions: builtins.list[str] | None = None, ) -> User: if not username: raise OctopError(ErrorCode.USERNAME_TAKEN, "username must not be empty") loc = normalize_locale(locale) validate_password_policy(password) + try: + keys = validate_permission_keys(permissions or []) + except ValueError as exc: + raise OctopError(ErrorCode.FORBIDDEN, str(exc), status=400) from exc async with self._lock: if self._services.user_repo.get_by_username(username) is not None: raise OctopError( @@ -91,6 +150,7 @@ async def create( role=role.value, display_name=display_name, locale=loc, + permissions=keys, ) user = User( id=uid, @@ -98,6 +158,7 @@ async def create( role=role, display_name=display_name, locale=loc, + permissions=keys, ) self._users[username] = user self._services.audit_repo.write(actor=username, action="user.create", target=username) @@ -128,6 +189,87 @@ def count(self) -> int: # ----- auth ----- + async def resolve_or_create_sso_user( + self, + *, + provider_id: int, + subject: str, + claims: dict[str, Any], + ) -> User: + async with self._lock: + row = self._services.user_repo.get_by_sso(provider_id, subject) + email = _normalized_claim_email(claims) + display_name = _claim_display_name(claims) + + if row is None: + for attempt in range(3): + if ( + email is not None + and self._services.user_repo.get_by_email(email) is not None + ): + email = None + username = allocate_username(self._services.user_repo, claims, subject) + try: + uid = self._services.user_repo.create( + username=username, + password_hash=None, + role=Role.USER.value, + display_name=display_name, + email=email, + sso_provider_id=provider_id, + sso_subject=subject, + ) + except Exception as exc: + if not _is_unique_violation(exc): + raise + row = self._services.user_repo.get_by_sso(provider_id, subject) + if row is not None: + break + if attempt == 2: + raise + await asyncio.sleep(0.01 * (attempt + 1)) + else: + user = User( + id=uid, + username=username, + role=Role.USER, + display_name=display_name, + permissions=[], + ) + self._users[username] = user + self._services.audit_repo.write( + actor=username, action="user.sso_create", target=username + ) + return user + + assert row is not None # The retry loop either returns, raises, or finds this identity. + if row.disabled: + raise OctopError(ErrorCode.USER_DISABLED, "user is disabled") + if email is not None: + email_owner = self._services.user_repo.get_by_email(email) + if email_owner is not None and email_owner.id != row.id: + email = None + self._services.user_repo.update_sso_profile( + row.id, + email=email, + display_name=display_name, + ) + cached_user = self._users.get(row.username) + if cached_user is None: + user = User( + id=row.id, + username=row.username, + role=Role(row.role), + display_name=display_name if display_name is not None else row.display_name, + locale=normalize_locale(row.locale), + permissions=list(row.permissions), + ) + self._users[row.username] = user + return user + if display_name is not None: + cached_user.display_name = display_name + return cached_user + async def authenticate(self, username: str, password: str) -> User | None: row = self._services.user_repo.get_by_username(username) if row is None: @@ -135,6 +277,8 @@ async def authenticate(self, username: str, password: str) -> User | None: now = int(time.time()) if row.disabled: return None + if not row.password_hash: + return None locked_until = int(row.login_locked_until or 0) if locked_until > now: retry_after = locked_until - now @@ -169,6 +313,7 @@ async def authenticate(self, username: str, password: str) -> User | None: role=Role(row.role), display_name=row.display_name, locale=normalize_locale(row.locale), + permissions=list(row.permissions), ) self._users[username] = user self._services.audit_repo.write(actor=username, action="auth.login") @@ -176,7 +321,7 @@ async def authenticate(self, username: str, password: str) -> User | None: async def change_password(self, username: str, old: str, new: str) -> None: row = self._services.user_repo.get_by_username(username) - if row is None or not verify_password(old, row.password_hash): + if row is None or not row.password_hash or not verify_password(old, row.password_hash): raise OctopError(ErrorCode.AUTH_FAILED, "current password incorrect") validate_password_policy(new, old_password=old) self._services.user_repo.set_password_hash(row.id, hash_password(new)) @@ -197,6 +342,26 @@ async def reset_password(self, username: str, new: str) -> None: actor=ACTOR_ADMIN, action="user.password_reset", target=username ) + async def set_permissions(self, username: str, permissions: builtins.list[str]) -> None: + row = self._services.user_repo.get_by_username(username) + if row is None: + raise OctopError(ErrorCode.NOT_FOUND, "user not found") + try: + keys = validate_permission_keys(permissions) + except ValueError as exc: + raise OctopError(ErrorCode.FORBIDDEN, str(exc), status=400) from exc + self._services.user_repo.set_permissions(row.id, keys) + async with self._lock: + current = self._users.get(username) + if current is not None: + current.permissions = list(keys) + self._services.audit_repo.write( + actor=ACTOR_ADMIN, + action="user.set_permissions", + target=username, + payload=",".join(keys), + ) + async def set_role(self, username: str, role: Role) -> None: row = self._services.user_repo.get_by_username(username) if row is None: @@ -290,6 +455,7 @@ async def enable(self, username: str) -> None: role=Role(row.role), display_name=row.display_name, locale=normalize_locale(row.locale), + permissions=list(row.permissions), ) async with self._lock: self._users[username] = user diff --git a/src/octop/infra/users/permissions.py b/src/octop/infra/users/permissions.py new file mode 100644 index 00000000..cba45724 --- /dev/null +++ b/src/octop/infra/users/permissions.py @@ -0,0 +1,245 @@ +"""Module-level permission catalog and checks. + +A permission is a module key (e.g. ``"browser"``, ``"users"``). Possessing a +key grants access to that module's management page and write/configure actions. +Read access and agent use in chat are never gated. ``admin`` bypasses all. + +Categories mirror dashboard nav groups: ``settings`` / ``control`` / ``admin``. +Admin keys may also carry a ``page`` so the picker can group by page / tab. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + + +class PermissionUser(Protocol): + @property + def is_admin(self) -> bool: ... + + permissions: list[str] + + +@dataclass(frozen=True) +class PermissionDef: + key: str + category: str # "settings" | "control" | "admin" + label_zh: str + label_en: str + page: str = "" + page_zh: str = "" + page_en: str = "" + extra_tabs: tuple[tuple[str, str], ...] = () + + +def _p( + key: str, + category: str, + label_zh: str, + label_en: str, + *, + page: str = "", + page_zh: str = "", + page_en: str = "", + extra_tabs: tuple[tuple[str, str], ...] = (), +) -> PermissionDef: + return PermissionDef(key, category, label_zh, label_en, page, page_zh, page_en, extra_tabs) + + +PERMISSIONS: dict[str, PermissionDef] = { + # --- settings (nav.settings) — listed & default-selected for new users --- + "channels": _p("channels", "settings", "通道", "Channels"), + "connectors": _p("connectors", "settings", "连接器", "Connectors"), + "skill_packages": _p("skill_packages", "settings", "技能包", "Skill Packages"), + "knowledge_bases": _p("knowledge_bases", "settings", "知识库", "Knowledge Base"), + # --- control (nav.control) — page/tab labels --- + "terminal": _p("terminal", "control", "工作台/终端", "Workbench / Terminal"), + "browser": _p("browser", "control", "工作台/浏览器", "Workbench / Browser"), + "desktop": _p("desktop", "control", "远程桌面", "Remote Desktop"), + # --- admin: grouped by page, chip = tab title --- + "users": _p( + "users", + "admin", + "内置用户", + "Local users", + page="users", + page_zh="用户", + page_en="Users", + ), + "sso": _p( + "sso", + "admin", + "单点登录", + "Single sign-on", + page="users", + page_zh="用户", + page_en="Users", + ), + "providers": _p( + "providers", + "admin", + "云端", + "Cloud", + page="models", + page_zh="模型", + page_en="Models", + ), + "ollama_models": _p( + "ollama_models", + "admin", + "Ollama", + "Ollama", + page="models", + page_zh="模型", + page_en="Models", + ), + "onnx_models": _p( + "onnx_models", + "admin", + "ONNX", + "ONNX", + page="models", + page_zh="模型", + page_en="Models", + ), + "storage_backends": _p("storage_backends", "admin", "存储", "Storage"), + "plugins": _p( + "plugins", + "admin", + "已安装", + "Installed", + page="plugins", + page_zh="插件", + page_en="Plugins", + ), + "security": _p( + "security", + "admin", + "防护策略", + "Policy", + page="security", + page_zh="安全防护", + page_en="Security", + ), + "admin_console": _p( + "admin_console", + "admin", + "审计日志", + "Audit log", + page="security", + page_zh="安全防护", + page_en="Security", + ), + "envs": _p( + "envs", + "admin", + "环境变量", + "Environment", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "search": _p( + "search", + "admin", + "AI 搜索", + "AI search", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "knowledge_settings": _p( + "knowledge_settings", + "admin", + "知识库设置", + "Knowledge-base settings", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "voice": _p( + "voice", + "admin", + "语音", + "Voice", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "observability": _p( + "observability", + "admin", + "可观测", + "Observability", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "backup": _p( + "backup", + "admin", + "备份与恢复", + "Backup & restore", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "tls": _p( + "tls", + "admin", + "HTTPS", + "HTTPS", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), + "update": _p( + "update", + "admin", + "应用更新", + "Updates", + page="advanced", + page_zh="应用设置", + page_en="App settings", + ), +} + +ALL_PERMISSION_KEYS: set[str] = set(PERMISSIONS) + +# Settings-group keys: shown in the picker and pre-checked for new users. +# They are still stored explicitly — not silently granted without being written. +BASELINE_PERMISSIONS: set[str] = {key for key, p in PERMISSIONS.items() if p.category == "settings"} + + +def user_has_permission(user: PermissionUser, key: str) -> bool: + """Return True if ``user`` may access the module ``key``. + + Unknown keys are denied. ``admin`` bypasses everything. + """ + if key not in PERMISSIONS: + return False + if user.is_admin: + return True + return key in (user.permissions or []) + + +def validate_permission_keys(keys: list[str]) -> list[str]: + """Return a deduped list or raise ``ValueError`` on unknown keys.""" + unknown = sorted({k for k in keys if k not in PERMISSIONS}) + if unknown: + raise ValueError(f"unknown permission keys: {unknown}") + seen: set[str] = set() + out: list[str] = [] + for k in keys: + if k not in seen: + seen.add(k) + out.append(k) + return out + + +def effective_permissions(user: PermissionUser) -> list[str]: + """Permissions to expose on ``/auth/me`` / login (admin gets the full catalog).""" + if user.is_admin: + return sorted(ALL_PERMISSION_KEYS) + return list(user.permissions or []) diff --git a/src/octop/infra/utils/ollama_manager.py b/src/octop/infra/utils/ollama_manager.py index c1adec4a..d7b0e1dc 100644 --- a/src/octop/infra/utils/ollama_manager.py +++ b/src/octop/infra/utils/ollama_manager.py @@ -136,6 +136,49 @@ def _ensure_ollama() -> Any: return sdk +def is_ollama_reachable() -> bool: + """Public wrapper: True when the Ollama HTTP endpoint responds.""" + return _is_ollama_reachable() + + +def start_ollama_service() -> None: + """Ensure the Ollama daemon is running (start if needed).""" + _ensure_ollama_server() + + +def stop_ollama_service() -> bool: + """Best-effort stop of an Ollama daemon we started; returns True if stopped.""" + global _OLLAMA_SERVER_STARTED + if not _OLLAMA_SERVER_STARTED and not _is_ollama_reachable(): + return True + import os + + stopped = False + try: + # Prefer polite terminate of `ollama serve` we may have spawned. + # Cross-platform: try pkill on POSIX; on Windows skip process kill. + if os.name == "posix": + subprocess.run( + ["pkill", "-f", "ollama serve"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # wait briefly + for _ in range(20): + if not _is_ollama_reachable(): + stopped = True + break + time.sleep(0.25) + else: + # Windows: leave daemon running; preference flag still gates auto-start. + stopped = not _is_ollama_reachable() + except Exception as exc: + logger.warning("Failed to stop ollama serve: %s", exc) + _OLLAMA_SERVER_STARTED = False + return stopped or not _is_ollama_reachable() + + class OllamaModelManager: """High-level wrapper around the Ollama SDK for model lifecycle.""" diff --git a/src/octop/infra/utils/paths.py b/src/octop/infra/utils/paths.py index 7f242a6f..583e5220 100644 --- a/src/octop/infra/utils/paths.py +++ b/src/octop/infra/utils/paths.py @@ -59,11 +59,21 @@ def expert_market_dir(self) -> Path: """Cached SkillHub expert templates: ``~/.octop/expert_market/``.""" return self.root / "expert_market" + @property + def published_experts_dir(self) -> Path: + """User-published expert snapshots: ``~/.octop/published_experts/``.""" + return self.root / "published_experts" + @property def skill_packages_dir(self) -> Path: """Global skill package content: ``~/.octop/skill-packages/``.""" return self.root / "skill-packages" + @property + def knowledge_dir(self) -> Path: + """Global knowledge base files: ``~/.octop/knowledge/``.""" + return self.root / "knowledge" + def agent_workspace(self, agent_id: str) -> Path: """Global agent workspace: ~/.octop/agents//""" return self.agents_dir / agent_id diff --git a/src/octop/infra/utils/runtime_packages.py b/src/octop/infra/utils/runtime_packages.py new file mode 100644 index 00000000..75f63662 --- /dev/null +++ b/src/octop/infra/utils/runtime_packages.py @@ -0,0 +1,284 @@ +"""Install optional Python packages into the active interpreter at runtime. + +Octop deployments vary: uv venvs often omit ``pip``, systemd services may not +have ``uv`` on ``PATH``, and some hosts only ship the stdlib. Callers should +use :func:`install_packages` / :func:`install_packages_async` instead of +hand-rolling ``pip`` / ``uv pip`` commands. +""" + +from __future__ import annotations + +import asyncio +import importlib +import importlib.util +import logging +import os +import shutil +import subprocess +import sys +import threading +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +logger = logging.getLogger(__name__) + +INSTALL_TIMEOUT_SEC = 600 +_PIP_MISSING_MARKERS = ("No module named pip", "No module named 'pip'") +_INSTALL_LOCK = threading.Lock() + +InstallOutcome = Literal["ready", "installed"] + + +@dataclass(frozen=True) +class PackageInstallSpec: + """Direct PyPI specs plus an optional ``octop[extra]`` fallback.""" + + packages: tuple[str, ...] + extra_fallback: str | None = None + + +def find_uv_binary() -> str | None: + """Resolve ``uv`` from env, PATH, or common install locations.""" + override = os.environ.get("OCTOP_UV_BIN", "").strip() + if override: + path = Path(override) + if path.is_file(): + return str(path) + found = shutil.which("uv") + if found: + return found + for candidate in _uv_search_paths(): + if candidate.is_file(): + return str(candidate) + return None + + +def _uv_search_paths() -> tuple[Path, ...]: + if os.name == "nt": + paths: list[Path] = [] + local_app = os.environ.get("LOCALAPPDATA", "").strip() + if local_app: + paths.append(Path(local_app) / "Programs" / "uv" / "uv.exe") + program_files = os.environ.get("PROGRAMFILES", "").strip() + if program_files: + paths.append(Path(program_files) / "uv" / "uv.exe") + paths.extend( + ( + Path.home() / ".local" / "bin" / "uv.exe", + Path.home() / ".cargo" / "bin" / "uv.exe", + ) + ) + return tuple(paths) + return ( + Path.home() / ".local" / "bin" / "uv", + Path.home() / ".cargo" / "bin" / "uv", + Path("/usr/local/bin/uv"), + Path("/bin/uv"), + ) + + +def pip_importable() -> bool: + return importlib.util.find_spec("pip") is not None + + +def ensure_pip_bootstrapped() -> bool: + """Best-effort ``ensurepip`` when the interpreter has no ``pip`` module.""" + if pip_importable(): + return True + logger.info("Bootstrapping pip via ensurepip for %s", sys.executable) + try: + proc = subprocess.run( + [sys.executable, "-m", "ensurepip", "--default-pip"], + capture_output=True, + text=True, + check=False, + timeout=120, + shell=False, + ) + except subprocess.TimeoutExpired: + return False + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + if detail: + logger.warning("ensurepip failed: %s", detail[-400:]) + return False + return pip_importable() + + +def purge_import_cache(module_names: Sequence[str]) -> None: + for name in module_names: + sys.modules.pop(name, None) + importlib.invalidate_caches() + + +def build_install_commands(packages: Sequence[str]) -> list[list[str]]: + """Ordered install strategies for *packages* into ``sys.executable``.""" + specs = list(packages) + commands: list[list[str]] = [] + uv_bin = find_uv_binary() + if uv_bin: + commands.append([uv_bin, "pip", "install", "--python", sys.executable, *specs]) + commands.append( + [ + sys.executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + *specs, + ] + ) + return commands + + +def _extra_fallback_commands(extra: str) -> list[list[str]]: + spec = f"octop[{extra}]" + commands: list[list[str]] = [] + uv_bin = find_uv_binary() + if uv_bin: + commands.append([uv_bin, "pip", "install", "--python", sys.executable, spec]) + commands.append( + [ + sys.executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + spec, + ] + ) + return commands + + +def _pip_missing_output(detail: str) -> bool: + return any(marker in detail for marker in _PIP_MISSING_MARKERS) + + +def _run_install_command(cmd: list[str], *, timeout: int) -> tuple[bool, str]: + logger.info("Installing runtime Python packages: %s", " ".join(cmd)) + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + timeout=timeout, + shell=False, + ) + except subprocess.TimeoutExpired: + return False, "timed out" + if proc.returncode == 0: + return True, "" + detail = (proc.stderr or proc.stdout or "").strip() + if len(detail) > 800: + detail = detail[-800:] + return False, detail + + +def _attempt_install_commands( + commands: Sequence[Sequence[str]], + *, + timeout: int, +) -> tuple[bool, str]: + last_detail = "" + pip_bootstrapped = False + for cmd in commands: + ok, detail = _run_install_command(list(cmd), timeout=timeout) + if ok: + return True, "" + last_detail = detail + if _pip_missing_output(detail) and not pip_bootstrapped: + pip_bootstrapped = ensure_pip_bootstrapped() + if pip_bootstrapped: + logger.info("Retrying pip install after ensurepip bootstrap") + ok, detail = _run_install_command(list(cmd), timeout=timeout) + if ok: + return True, "" + last_detail = detail + logger.warning("pip unavailable; trying next installer strategy") + return False, last_detail + + +def install_packages( + spec: PackageInstallSpec | Sequence[str], + *, + is_satisfied: Callable[[], bool] | None = None, + import_modules: Sequence[str] = (), + timeout: int = INSTALL_TIMEOUT_SEC, +) -> InstallOutcome: + """Install *spec* into the running interpreter with multiple fallbacks.""" + if isinstance(spec, PackageInstallSpec): + packages = spec.packages + extra_fallback = spec.extra_fallback + else: + packages = tuple(spec) + extra_fallback = None + + if is_satisfied is not None and is_satisfied(): + return "ready" + + commands: list[list[str]] = list(build_install_commands(packages)) + if extra_fallback: + commands.extend(_extra_fallback_commands(extra_fallback)) + + with _INSTALL_LOCK: + if is_satisfied is not None and is_satisfied(): + return "ready" + + ok, detail = _attempt_install_commands(commands, timeout=timeout) + if import_modules: + purge_import_cache(import_modules) + + if is_satisfied is not None: + if not is_satisfied(): + raise RuntimeError(_install_failed_message(detail if not ok else "")) + return "installed" if ok else "ready" + + if not ok: + raise RuntimeError(_install_failed_message(detail)) + return "installed" + + +async def install_packages_async( + spec: PackageInstallSpec | Sequence[str], + *, + is_satisfied: Callable[[], bool] | None = None, + import_modules: Sequence[str] = (), + timeout: int = INSTALL_TIMEOUT_SEC, +) -> InstallOutcome: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, + lambda: install_packages( + spec, + is_satisfied=is_satisfied, + import_modules=import_modules, + timeout=timeout, + ), + ) + + +def build_uninstall_command(packages: Sequence[str]) -> list[str] | None: + """First-choice uninstall command for *packages*, or ``None`` when unavailable.""" + names = list(packages) + uv_bin = find_uv_binary() + if uv_bin: + return [uv_bin, "pip", "uninstall", "-y", *names, "--python", sys.executable] + if pip_importable(): + return [sys.executable, "-m", "pip", "uninstall", "-y", *names] + return None + + +def _install_failed_message(detail: str) -> str: + """User-facing failure text — no shell commands.""" + base = ( + "Could not install optional Python components automatically. " + "Check network access and server logs, then retry." + ) + if not detail or detail == "timed out": + return base + if _pip_missing_output(detail): + return f"{base} This Python environment has no package installer (pip/uv unavailable)." + return base diff --git a/tests/integration/test_agents_shared.py b/tests/integration/test_agents_shared.py new file mode 100644 index 00000000..12ffc022 --- /dev/null +++ b/tests/integration/test_agents_shared.py @@ -0,0 +1,240 @@ +"""Shared agent list and ownership behavior.""" + +from __future__ import annotations + +import pytest + +from tests.support.auth import create_user + + +@pytest.fixture +async def env(env_with_provider): + yield env_with_provider + + +async def _shared_agent(env) -> tuple[object, dict[str, str], dict[str, str], str]: + client, _srv, admin_auth = env + owner_auth = await create_user(client, admin_auth, username="share_owner") + peer_auth = await create_user(client, admin_auth, username="share_peer") + response = await client.post( + "/api/agents/from-expert/default", + headers=owner_auth, + json={"name": "shared-bot"}, + ) + assert response.status_code == 201, response.text + return client, owner_auth, peer_auth, response.json()["agent_id"] + + +async def test_shared_agent_appears_for_other_user(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + assert response.json()["is_shared"] is True + + response = await client.get("/api/agents", headers=peer_auth) + assert response.status_code == 200 + agents = {row["agent_id"]: row for row in response.json()} + assert agent_id in agents + assert agents[agent_id]["is_owner"] is False + assert agents[agent_id]["owner_username"] == "share_owner" + + response = await client.get(f"/api/agents/{agent_id}", headers=peer_auth) + assert response.status_code == 200 + assert response.json()["is_shared"] is True + assert response.json()["is_owner"] is False + + +async def test_create_agent_can_be_shared(env) -> None: + client, _srv, admin_auth = env + owner_auth = await create_user(client, admin_auth, username="shared_create_owner") + peer_auth = await create_user(client, admin_auth, username="shared_create_peer") + + response = await client.post( + "/api/agents", + headers=owner_auth, + json={"name": "created-shared-bot", "config": {}, "is_shared": True}, + ) + assert response.status_code == 201, response.text + agent_id = response.json()["agent_id"] + assert response.json()["is_shared"] is True + + response = await client.get("/api/agents", headers=peer_auth) + assert response.status_code == 200 + assert agent_id in {row["agent_id"] for row in response.json()} + + +async def test_peer_cannot_patch_shared_agent(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.patch( + f"/api/agents/{agent_id}", + headers=peer_auth, + json={"name": "hijack"}, + ) + assert response.status_code == 403 + + +async def test_peer_cannot_mutate_shared_agent_workspace_or_skills(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.put( + f"/api/agents/{agent_id}/workspace/file", + headers=peer_auth, + params={"path": "SOUL.md"}, + json={"content": "peer mutation"}, + ) + assert response.status_code == 403 + + response = await client.post( + f"/api/agents/{agent_id}/skills/example/disable", + headers=peer_auth, + ) + assert response.status_code == 403 + + response = await client.post( + "/api/mbti/apply", + headers={**peer_auth, "X-Octop-Agent-Id": agent_id}, + json={"code": "INTJ"}, + ) + assert response.status_code == 403 + + response = await client.get( + f"/api/agents/{agent_id}/terminal/context", + headers=peer_auth, + ) + assert response.status_code == 403 + + response = await client.get( + f"/api/agents/{agent_id}/chat/welcome", + headers=peer_auth, + ) + assert response.status_code == 200, response.text + assert "welcome_message" in response.json() + + response = await client.get(f"/api/agents/{agent_id}/threads", headers=peer_auth) + assert response.status_code == 200, response.text + + +async def test_peer_can_read_shared_agent_inbound_attachment(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.put( + f"/api/agents/{agent_id}/workspace/file", + headers=owner_auth, + params={"path": "inbound/shared-attachment.txt"}, + json={"content": "shared attachment"}, + ) + assert response.status_code == 200, response.text + + response = await client.get( + f"/api/agents/{agent_id}/workspace/file", + headers=peer_auth, + params={"path": "inbound/shared-attachment.txt"}, + ) + assert response.status_code == 200, response.text + assert response.json()["content"] == "shared attachment" + + response = await client.get( + f"/api/agents/{agent_id}/workspace/download", + headers=peer_auth, + params={"path": "inbound/shared-attachment.txt"}, + ) + assert response.status_code == 200, response.text + assert response.content == b"shared attachment" + + +async def test_unshare_hides_agent_from_peer(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": False}, + ) + assert response.status_code == 200, response.text + assert response.json()["is_shared"] is False + + response = await client.get("/api/agents", headers=peer_auth) + assert response.status_code == 200 + assert agent_id not in {row["agent_id"] for row in response.json()} + + response = await client.get(f"/api/agents/{agent_id}", headers=peer_auth) + assert response.status_code == 403 + + +async def test_shared_agent_threads_are_private(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.post(f"/api/agents/{agent_id}/threads", headers=owner_auth) + assert response.status_code == 201, response.text + owner_thread_id = response.json()["thread_id"] + + response = await client.post(f"/api/agents/{agent_id}/threads", headers=peer_auth) + assert response.status_code == 201, response.text + peer_thread_id = response.json()["thread_id"] + + response = await client.get(f"/api/agents/{agent_id}/threads", headers=peer_auth) + assert response.status_code == 200, response.text + assert {thread["thread_id"] for thread in response.json()} == {peer_thread_id} + + response = await client.get( + f"/api/agents/{agent_id}/threads/{owner_thread_id}/history", + headers=peer_auth, + ) + assert response.status_code == 403 + + +async def test_peer_cannot_rebind_session_to_owner_thread(env) -> None: + client, owner_auth, peer_auth, agent_id = await _shared_agent(env) + response = await client.patch( + f"/api/agents/{agent_id}", + headers=owner_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await client.post(f"/api/agents/{agent_id}/threads", headers=owner_auth) + assert response.status_code == 201, response.text + owner_thread_id = response.json()["thread_id"] + + response = await client.patch( + f"/api/agents/{agent_id}/session", + headers=peer_auth, + json={"thread_id": owner_thread_id}, + ) + assert response.status_code == 403 diff --git a/tests/integration/test_auth_oidc.py b/tests/integration/test_auth_oidc.py new file mode 100644 index 00000000..d18bc4c6 --- /dev/null +++ b/tests/integration/test_auth_oidc.py @@ -0,0 +1,133 @@ +"""OIDC HTTP route integration tests.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from octop.infra.auth.sso.service import RedirectResult, SsoService +from tests.support.auth import bearer, bootstrap_admin, login + + +@pytest.fixture +async def client(app_client): + yield app_client + + +async def test_oidc_public_routes_and_exchange_return_login_shape( + client, monkeypatch: pytest.MonkeyPatch +) -> None: + c, srv, home = client + await bootstrap_admin(c, home) + assert srv.user_manager is not None + user = srv.user_manager.get("admin") + assert user is not None + + monkeypatch.setattr( + SsoService, + "start_login", + lambda self, *, redirect_after, public_base: { + "authorization_url": ( + f"https://idp.example/authorize?redirect={redirect_after}&state=browser-state" + ), + "state": "browser-state", + }, + ) + + callback_states: list[str | None] = [] + + async def callback(self: SsoService, **_: Any) -> RedirectResult: + callback_states.append(_["state"]) + return RedirectResult("http://testserver/login/oidc/complete#code=one-time") + + async def exchange(self: SsoService, code: str): + assert code == "one-time" + return user + + monkeypatch.setattr(SsoService, "handle_callback", callback) + monkeypatch.setattr(SsoService, "exchange_login_code", exchange) + + status = await c.get("/api/auth/oidc/status") + assert status.status_code == 200 + assert status.json() == {"enabled": False, "display_name": ""} + + start = await c.post("/api/auth/oidc/start", json={"redirect_after": "/chat"}) + assert start.status_code == 200 + assert "redirect=/chat" in start.json()["authorization_url"] + assert start.cookies["octop_oidc_state"] == "browser-state" + set_cookie = start.headers["set-cookie"] + assert "HttpOnly" in set_cookie + assert "Max-Age=600" in set_cookie + assert "Path=/api/auth/oidc" in set_cookie + assert "SameSite=lax" in set_cookie + + callback_response = await c.get( + "/api/auth/oidc/callback", params={"code": "provider-code", "state": "attacker-state"} + ) + assert callback_response.status_code == 302 + assert callback_response.headers["location"] == "http://testserver/login?oidc_error=state" + assert callback_states == [] + assert "octop_oidc_state=" in callback_response.headers["set-cookie"] + assert "Max-Age=0" in callback_response.headers["set-cookie"] + + start = await c.post("/api/auth/oidc/start", json={"redirect_after": "/chat"}) + callback_response = await c.get( + "/api/auth/oidc/callback", params={"code": "provider-code", "state": "browser-state"} + ) + assert callback_response.status_code == 302 + assert callback_response.headers["location"].endswith("#code=one-time") + assert callback_states == ["browser-state"] + + exchange_response = await c.post("/api/auth/oidc/exchange", json={"code": "one-time"}) + assert exchange_response.status_code == 200 + body = exchange_response.json() + assert body["token_type"] == "Bearer" + assert body["expires_in"] == srv.services.config.access_token_ttl_seconds + assert body["user"]["username"] == "admin" + assert body["access_token"] + + +async def test_oidc_config_requires_admin_and_returns_public_callback_url( + client, monkeypatch: pytest.MonkeyPatch +) -> None: + c, _srv, home = client + await bootstrap_admin(c, home) + token = await login(c) + + assert (await c.get("/api/auth/oidc/config")).status_code == 401 + + config = await c.get("/api/auth/oidc/config", headers=bearer(token)) + assert config.status_code == 200 + assert config.json()["redirect_uri"] == "http://testserver/api/auth/oidc/callback" + + update = await c.put( + "/api/auth/oidc/config", + headers=bearer(token), + json={ + "enabled": False, + "display_name": "Company Login", + "issuer": "https://issuer.example", + "client_id": "octop-client", + "dashboard_origin": "https://dashboard.example", + }, + ) + assert update.status_code == 200 + assert update.json()["display_name"] == "Company Login" + assert update.json()["redirect_uri"] == "http://testserver/api/auth/oidc/callback" + assert "client_secret" not in update.json() + + config = await c.get( + "/api/auth/oidc/config", + headers={**bearer(token), "X-Forwarded-Proto": "https", "X-Forwarded-Host": "api.example"}, + ) + assert config.status_code == 200 + assert config.json()["dashboard_origin"] == "https://dashboard.example" + assert config.json()["redirect_uri"] == "https://api.example/api/auth/oidc/callback" + + monkeypatch.setattr( + SsoService, "test_connection", lambda self: {"ok": True, "detail": "reachable"} + ) + tested = await c.post("/api/auth/oidc/config/test", headers=bearer(token)) + assert tested.status_code == 200 + assert tested.json() == {"ok": True, "detail": "reachable"} diff --git a/tests/integration/test_bwrap_jail.py b/tests/integration/test_bwrap_jail.py index 232c5c2d..e4ff3caa 100644 --- a/tests/integration/test_bwrap_jail.py +++ b/tests/integration/test_bwrap_jail.py @@ -6,6 +6,7 @@ from __future__ import annotations +import os import shutil import uuid from pathlib import Path @@ -208,6 +209,8 @@ def test_subprocess_run_invokes_real_bwrap(tmp_path: Path) -> None: marker = "octop-bwrap-smoke" bwrap = shutil.which("bwrap") assert bwrap is not None + # Merged-/usr hosts expose /bin,/lib,/lib64 as symlinks into /usr; bind those + # as symlinks (and always mount /usr) so the dynamic linker and /bin/sh resolve. argv = [ bwrap, "--die-with-parent", @@ -217,22 +220,29 @@ def test_subprocess_run_invokes_real_bwrap(tmp_path: Path) -> None: "--ro-bind", "/usr", "/usr", - "--ro-bind", - "/bin", - "/bin", - "--dev", - "/dev", - "--proc", - "/proc", - "--tmpfs", - "/tmp", - "--chdir", - "/", - "--", - "/bin/sh", - "-c", - f"echo {marker} > /smoke.txt", ] + for host, guest in (("/lib", "/lib"), ("/lib64", "/lib64"), ("/bin", "/bin")): + path = Path(host) + if path.is_symlink(): + argv.extend(["--symlink", os.readlink(host), guest]) + elif path.exists(): + argv.extend(["--ro-bind", host, guest]) + argv.extend( + [ + "--dev", + "/dev", + "--proc", + "/proc", + "--tmpfs", + "/tmp", + "--chdir", + "/", + "--", + "/bin/sh", + "-c", + f"echo {marker} > /smoke.txt", + ] + ) result = subprocess.run(argv, check=False, capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stderr assert (root / "smoke.txt").read_text(encoding="utf-8").strip() == marker diff --git a/tests/integration/test_chat_ws.py b/tests/integration/test_chat_ws.py index 67a298ae..b1ac8e78 100644 --- a/tests/integration/test_chat_ws.py +++ b/tests/integration/test_chat_ws.py @@ -246,6 +246,29 @@ async def test_ws_subscribe_turn_status_idle(env: Any) -> None: assert frame == {"type": "turn_status", "thread_id": tid, "active": False} +async def test_ws_subscribe_rejects_another_users_thread(env: Any) -> None: + c, _srv, _fake, alice_auth, bob_auth, aid = env + response = await c.patch( + f"/api/agents/{aid}", + headers=alice_auth, + json={"is_shared": True}, + ) + assert response.status_code == 200, response.text + + response = await c.post(f"/api/agents/{aid}/threads", headers=bob_auth) + assert response.status_code == 201, response.text + tid = response.json()["thread_id"] + + frame = await asyncio.to_thread( + _subscribe_ws_sync, + c._octop_app, # type: ignore[attr-defined] + aid, + ws_token(alice_auth), + tid, + ) + assert frame == {"type": "error", "message": f"thread {tid!r} not found"} + + def _cancel_ws_turn_sync( app: object, aid: str, @@ -390,3 +413,82 @@ async def test_create_thread(env: Any) -> None: body = r.json() assert "thread_id" in body assert "session_key" in body + + +async def test_fork_thread_from_user_message(env: Any) -> None: + from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + + c, srv, _fake, alice_auth, bob_auth, aid = env + created = await c.post(f"/api/agents/{aid}/threads", headers=alice_auth) + source_id = created.json()["thread_id"] + agent = srv.app_runtime.agent_registry.get_agent(aid) + agent.seed_thread_messages( + source_id, + [ + HumanMessage(content="first question", id="h1"), + AIMessage(content="first answer", id="a1"), + ToolMessage(content="tool-out", id="t1", tool_call_id="c1"), + HumanMessage(content="second question", id="h2"), + AIMessage(content="second answer", id="a2"), + ], + ) + + forked = await c.post( + f"/api/agents/{aid}/threads/{source_id}/fork", + headers=alice_auth, + json={ + "message_id": "h2", + "content": "second question", + "user_turns_from_end": 1, + }, + ) + assert forked.status_code == 201, forked.text + body = forked.json() + dest_id = body["thread_id"] + assert dest_id != source_id + assert body["source_thread_id"] == source_id + assert body["copied_messages"] == 3 + + history = await c.get( + f"/api/agents/{aid}/threads/{dest_id}/history", + headers=alice_auth, + ) + assert history.status_code == 200 + roles = [m["role"] for m in history.json()["messages"]] + texts = [] + for msg in history.json()["messages"]: + content = msg.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + texts.append(str(block.get("text") or "")) + elif isinstance(block, dict) and block.get("type") == "tool_result": + texts.append(str(block.get("output") or "")) + assert "user" in roles + assert "first question" in texts + assert "second question" not in texts + + source_history = await c.get( + f"/api/agents/{aid}/threads/{source_id}/history", + headers=alice_auth, + ) + source_texts: list[str] = [] + for msg in source_history.json()["messages"]: + content = msg.get("content") + if isinstance(content, str): + source_texts.append(content) + elif isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + source_texts.append(str(block.get("text") or "")) + assert "first question" in source_texts + assert "second question" in source_texts + + denied = await c.post( + f"/api/agents/{aid}/threads/{source_id}/fork", + headers=bob_auth, + json={"message_id": "h2", "user_turns_from_end": 1}, + ) + assert denied.status_code in {403, 404} diff --git a/tests/integration/test_connectors_api.py b/tests/integration/test_connectors_api.py index 2c435bed..690d9031 100644 --- a/tests/integration/test_connectors_api.py +++ b/tests/integration/test_connectors_api.py @@ -251,7 +251,7 @@ async def test_catalog_cli_connectors_last(env): async def test_install_cli_forbidden_for_non_admin(env): c, _, admin_auth, _ = env - user_auth = await create_user(c, admin_auth, username="cli_user") + user_auth = await create_user(c, admin_auth, username="cli_user", permissions=[]) r = await c.post("/api/connectors/feishu-cli/install-cli", headers=user_auth) assert r.status_code == 403 assert r.json()["error"]["code"] == "FORBIDDEN" diff --git a/tests/integration/test_knowledge_bases_api.py b/tests/integration/test_knowledge_bases_api.py new file mode 100644 index 00000000..a5a32533 --- /dev/null +++ b/tests/integration/test_knowledge_bases_api.py @@ -0,0 +1,50 @@ +"""HTTP ACL for knowledge-base instance settings vs page CRUD.""" + +from __future__ import annotations + +from typing import Any + +from tests.support.auth import create_user + + +async def test_feature_toggle_requires_knowledge_settings(env: Any) -> None: + client, _server, admin_auth = env + page_auth = await create_user(client, admin_auth, username="kb_page_user") + denied = await client.put( + "/api/knowledge-bases/feature", + headers=page_auth, + json={"enabled": False}, + ) + assert denied.status_code == 403, denied.text + assert denied.json()["error"]["code"] == "FORBIDDEN" + + settings_auth = await create_user( + client, + admin_auth, + username="kb_settings_user", + permissions=["knowledge_bases", "knowledge_settings"], + ) + allowed = await client.put( + "/api/knowledge-bases/feature", + headers=settings_auth, + json={"enabled": False}, + ) + assert allowed.status_code == 200, allowed.text + assert allowed.json()["feature_enabled"] is False + + +async def test_embedding_options_require_knowledge_settings(env: Any) -> None: + client, _server, admin_auth = env + page_auth = await create_user(client, admin_auth, username="kb_options_user") + denied = await client.get("/api/knowledge-bases/embedding-options", headers=page_auth) + assert denied.status_code == 403, denied.text + + settings_auth = await create_user( + client, + admin_auth, + username="kb_options_admin", + permissions=["knowledge_settings"], + ) + allowed = await client.get("/api/knowledge-bases/embedding-options", headers=settings_auth) + assert allowed.status_code == 200, allowed.text + assert "onnx" in allowed.json() diff --git a/tests/integration/test_provider_test_endpoint.py b/tests/integration/test_provider_test_endpoint.py index d6efb8af..425c0cc7 100644 --- a/tests/integration/test_provider_test_endpoint.py +++ b/tests/integration/test_provider_test_endpoint.py @@ -40,3 +40,30 @@ async def test_provider_test_missing_provider(env_with_provider_record: Any) -> c, _srv, auth, _pid = env_with_provider_record r = await c.post("/api/admin/providers/99999/test", headers=auth) assert r.status_code == 404 + + +async def test_provider_test_embedding_skips_chat_probe( + env_with_provider_record: Any, +) -> None: + c, _srv, auth, pid = env_with_provider_record + + async def fake_probe( + row: Any, *, model_id: str | None = None, embedding: bool | None = None + ) -> dict[str, Any]: + assert embedding is True + assert model_id == "text-embedding-3-small" + return {"ok": True, "latency_ms": 12} + + with patch( + "octop.api.routers.providers.probe_provider_row", + new=AsyncMock(side_effect=fake_probe), + ): + r = await c.post( + f"/api/admin/providers/{pid}/test", + headers=auth, + json={"model_id": "text-embedding-3-small", "embedding": True}, + ) + assert r.status_code == 200, r.text + body = r.json() + assert body["ok"] is True + assert body["latency_ms"] == 12 diff --git a/tests/integration/test_providers_api.py b/tests/integration/test_providers_api.py index a66fde04..74b522ab 100644 --- a/tests/integration/test_providers_api.py +++ b/tests/integration/test_providers_api.py @@ -70,3 +70,23 @@ async def test_admin_delete_provider(env): r = await c.get("/api/admin/providers", headers=auth) ids = [p["id"] for p in r.json()] assert pid not in ids + + +async def test_admin_cannot_delete_local_runtime_provider(env): + """DELETE /admin/providers/{id} keeps ONNX / Ollama local rows.""" + c, _, auth = env + r = await c.post( + "/api/admin/providers", + headers=auth, + json={"name": "ONNX (Local)", "kind": "openai", "api_key": "onnx"}, + ) + assert r.status_code == 201 + pid = r.json()["id"] + + r = await c.delete(f"/api/admin/providers/{pid}", headers=auth) + assert r.status_code == 409 + assert r.json()["error"]["code"] == "PROVIDER_LOCAL_PROTECTED" + + r = await c.get("/api/admin/providers", headers=auth) + ids = [p["id"] for p in r.json()] + assert pid in ids diff --git a/tests/integration/test_published_experts.py b/tests/integration/test_published_experts.py new file mode 100644 index 00000000..e3f0b4b1 --- /dev/null +++ b/tests/integration/test_published_experts.py @@ -0,0 +1,164 @@ +"""Published expert HTTP API integration coverage.""" + +from __future__ import annotations + +from typing import Any + +from tests.support.auth import create_user + + +async def _owner_and_peer( + env: tuple[Any, Any, dict[str, str]], +) -> tuple[Any, Any, dict[str, str], dict[str, str], str]: + client, server, admin_auth = env + owner_auth = await create_user(client, admin_auth, username="publish_owner") + peer_auth = await create_user(client, admin_auth, username="publish_peer") + created = await client.post( + "/api/agents/from-expert/default", + headers=owner_auth, + json={"name": "publish-source"}, + ) + assert created.status_code == 201, created.text + return client, server, owner_auth, peer_auth, created.json()["agent_id"] + + +async def test_publish_list_install_and_unpublish_preserves_installed_fork( + env: tuple[Any, Any, dict[str, str]], +) -> None: + client, _server, owner_auth, peer_auth, source_agent_id = await _owner_and_peer(env) + + published = await client.post( + f"/api/agents/{source_agent_id}/publish-expert", + headers=owner_auth, + json={"name": "Published default", "description": "A private forkable expert"}, + ) + assert published.status_code == 201, published.text + expert = published.json() + expert_id = expert["id"] + assert expert["creator_username"] == "publish_owner" + + listed = await client.get("/api/experts/published", headers=peer_auth) + assert listed.status_code == 200, listed.text + assert [row["id"] for row in listed.json()] == [expert_id] + + detail = await client.get(f"/api/experts/published/{expert_id}", headers=peer_auth) + assert detail.status_code == 200, detail.text + assert "manifest.json" in detail.json()["files"] + + installed = await client.post( + f"/api/experts/published/{expert_id}/install", + headers=peer_auth, + json={"name": "Peer installed expert", "description": "Peer copy"}, + ) + assert installed.status_code == 201, installed.text + installed_agent_id = installed.json()["agent_id"] + assert installed.json()["user_id"] != expert["created_by"] + + denied = await client.delete(f"/api/experts/published/{expert_id}", headers=peer_auth) + assert denied.status_code == 403, denied.text + + unpublished = await client.delete(f"/api/experts/published/{expert_id}", headers=owner_auth) + assert unpublished.status_code == 204, unpublished.text + assert (await client.get("/api/experts/published", headers=peer_auth)).json() == [] + + fork = await client.get(f"/api/agents/{installed_agent_id}", headers=peer_auth) + assert fork.status_code == 200, fork.text + assert fork.json()["name"] == "Peer installed expert" + + +async def test_install_published_expert_accepts_create_options( + env: tuple[Any, Any, dict[str, str]], +) -> None: + client, _server, owner_auth, peer_auth, source_agent_id = await _owner_and_peer(env) + + published = await client.post( + f"/api/agents/{source_agent_id}/publish-expert", + headers=owner_auth, + json={"name": "Parametric published", "description": "source"}, + ) + assert published.status_code == 201, published.text + expert_id = published.json()["id"] + + installed = await client.post( + f"/api/experts/published/{expert_id}/install", + headers=peer_auth, + json={ + "name": "Installed with options", + "description": "fork with options", + "default_model": "openai/gpt-4o-mini", + "max_iters": 12, + "backend": {"type": "local_shell"}, + }, + ) + assert installed.status_code == 201, installed.text + agent_id = installed.json()["agent_id"] + + detail = await client.get(f"/api/agents/{agent_id}", headers=peer_auth) + assert detail.status_code == 200, detail.text + body = detail.json() + assert body["name"] == "Installed with options" + assert body["default_model"] == "openai/gpt-4o-mini" + assert body.get("max_iters") == 12 + config = body.get("config") or {} + assert (config.get("backend") or {}).get("type") == "local_shell" + + +async def test_creator_can_refresh_published_expert( + env: tuple[Any, Any, dict[str, str]], +) -> None: + client, _server, owner_auth, _peer_auth, source_agent_id = await _owner_and_peer(env) + published = await client.post( + f"/api/agents/{source_agent_id}/publish-expert", + headers=owner_auth, + json={"name": "Refreshable expert"}, + ) + assert published.status_code == 201, published.text + before = published.json()["updated_at"] + + refreshed = await client.post( + f"/api/experts/published/{published.json()['id']}/refresh", + headers=owner_auth, + ) + assert refreshed.status_code == 200, refreshed.text + assert refreshed.json()["id"] == published.json()["id"] + assert refreshed.json()["updated_at"] >= before + + +async def test_second_publish_of_same_source_is_rejected( + env: tuple[Any, Any, dict[str, str]], +) -> None: + client, _server, owner_auth, _peer_auth, source_agent_id = await _owner_and_peer(env) + first = await client.post( + f"/api/agents/{source_agent_id}/publish-expert", + headers=owner_auth, + json={"name": "First publish"}, + ) + assert first.status_code == 201, first.text + + second = await client.post( + f"/api/agents/{source_agent_id}/publish-expert", + headers=owner_auth, + json={"name": "Duplicate publish"}, + ) + assert second.status_code == 409, second.text + assert second.json()["error"]["code"] == "PUBLISHED_EXPERT_ALREADY_EXISTS" + + +async def test_admin_cannot_publish_another_users_agent( + env: tuple[Any, Any, dict[str, str]], +) -> None: + client, _server, admin_auth = env + owner_auth = await create_user(client, admin_auth, username="private_agent_owner") + created = await client.post( + "/api/agents/from-expert/default", + headers=owner_auth, + json={"name": "private source"}, + ) + assert created.status_code == 201, created.text + + published = await client.post( + f"/api/agents/{created.json()['agent_id']}/publish-expert", + headers=admin_auth, + json={"name": "Admin must not publish"}, + ) + assert published.status_code == 403, published.text diff --git a/tests/integration/test_skills_api.py b/tests/integration/test_skills_api.py index 1b38ad85..090f9c42 100644 --- a/tests/integration/test_skills_api.py +++ b/tests/integration/test_skills_api.py @@ -111,6 +111,10 @@ async def test_list_includes_builtin_skills(env: Any) -> None: assert ws["enabled"] is True assert ws["emoji"] == "🔍" + manager = next(row for row in builtin if row["name"] == "skill-manager") + assert manager["enabled"] is True + assert "SkillHub" in manager["description"] + async def test_get_builtin_skill_detail(env: Any) -> None: c, _srv, auth, aid = env @@ -143,6 +147,20 @@ async def test_delete_builtin_skill_rejected(env: Any) -> None: assert r.status_code == 404 +async def test_octop_skill_manager_detail_and_delete_protection(env: Any) -> None: + c, _srv, auth, aid = env + + r = await c.get(f"/api/agents/{aid}/skills/skill-manager", headers=auth) + assert r.status_code == 200 + body = r.json() + assert body["kind"] == "builtin" + assert body["frontmatter"]["name"] == "skill-manager" + assert "manage_skills.py" in body["body"] + + r = await c.delete(f"/api/agents/{aid}/skills/skill-manager", headers=auth) + assert r.status_code == 404 + + async def test_workspace_skill_overrides_builtin_name(env: Any) -> None: c, _srv, auth, aid = env await _seed_builtin_skill(env) diff --git a/tests/integration/test_subagents_api.py b/tests/integration/test_subagents_api.py index d64cb571..5bc8d4da 100644 --- a/tests/integration/test_subagents_api.py +++ b/tests/integration/test_subagents_api.py @@ -10,6 +10,7 @@ name: Code Reviewer id: code-reviewer description: Review code changes for bugs and style issues +color: "#4B74FA" --- # Code Reviewer @@ -51,8 +52,11 @@ async def test_list_includes_workspace_subagent(env: Any) -> None: r = await c.get(f"/api/agents/{aid}/subagents", headers=auth) assert r.status_code == 200, r.text - slugs = {row["slug"] for row in r.json()} + rows = r.json() + slugs = {row["slug"] for row in rows} assert "code-reviewer" in slugs + reviewer = next(row for row in rows if row["slug"] == "code-reviewer") + assert reviewer["color"] == "#4B74FA" async def test_list_requires_auth(env: Any) -> None: diff --git a/tests/integration/test_users_api.py b/tests/integration/test_users_api.py index 2e9fef90..0907c68a 100644 --- a/tests/integration/test_users_api.py +++ b/tests/integration/test_users_api.py @@ -19,6 +19,9 @@ async def test_create_list_get_delete(env): r = await c.get(f"/api/users/{uid}", headers=auth) assert r.json()["username"] == "alice" + assert r.json()["email"] is None + assert r.json()["has_password"] is True + assert r.json()["sso_linked"] is False assert r.json()["login_locked"] is False assert r.json()["login_retry_after_seconds"] == 0 assert isinstance(r.json()["created_at"], int) diff --git a/tests/support/auth.py b/tests/support/auth.py index e2eeb2b6..e0b498e5 100644 --- a/tests/support/auth.py +++ b/tests/support/auth.py @@ -133,11 +133,22 @@ async def create_user( username: str, password: str = TEST_PASSWORD, role: str = "user", + permissions: list[str] | None = None, ) -> dict[str, str]: + from octop.infra.users.permissions import BASELINE_PERMISSIONS + + body: dict[str, object] = { + "username": username, + "password": password, + "role": role, + "permissions": ( + list(permissions) if permissions is not None else sorted(BASELINE_PERMISSIONS) + ), + } r = await client.post( "/api/users", headers=admin_auth, - json={"username": username, "password": password, "role": role}, + json=body, ) r.raise_for_status() return await auth_header(client, username=username, password=password) diff --git a/tests/support/fakes.py b/tests/support/fakes.py index eca00b78..1328ecd3 100644 --- a/tests/support/fakes.py +++ b/tests/support/fakes.py @@ -64,6 +64,8 @@ def __init__( ) self._mcp_tools: list[Any] = [] self._mcp_tool_name_set: frozenset[str] = frozenset() + self._thread_messages: dict[str, list[Any]] = {} + self.graph = _FakeHarnessGraph(self._thread_messages) def use_workspace_dir(self, workspace_dir: Path, *, virtual_mode: bool = False) -> None: """Bind workspace I/O to a real ``local_shell`` backend on disk. @@ -171,6 +173,8 @@ async def list_subagent_summaries(self) -> list[dict[str, Any]]: } if meta.get("emoji"): row["emoji"] = meta["emoji"] + if meta.get("color"): + row["color"] = str(meta["color"]).strip() out.append(row) return out @@ -298,6 +302,34 @@ async def list_skill_summaries(self) -> list[dict[str, Any]]: ), ) + async def aget_history( + self, thread_id: str, *, limit: int = 50, before: str | None = None + ) -> list[Any]: + del before + return list(self._thread_messages.get(thread_id, []))[-limit:] + + def seed_thread_messages(self, thread_id: str, messages: list[Any]) -> None: + self._thread_messages[thread_id] = list(messages) + + +class _FakeHarnessGraph: + """In-memory LangGraph stand-in for checkpoint read/write in tests.""" + + def __init__(self, store: dict[str, list[Any]]) -> None: + self._store = store + + async def aget_state(self, config: dict[str, Any]) -> SimpleNamespace: + thread_id = str((config.get("configurable") or {}).get("thread_id") or "") + return SimpleNamespace(values={"messages": list(self._store.get(thread_id, []))}) + + async def aupdate_state( + self, config: dict[str, Any], values: dict[str, Any], **_kwargs: Any + ) -> None: + thread_id = str((config.get("configurable") or {}).get("thread_id") or "") + incoming = values.get("messages") or [] + bucket = self._store.setdefault(thread_id, []) + bucket.extend(list(incoming)) + class _FakeBackend: """In-memory backend supporting the harness BackendProtocol surface used by Octop routers.""" diff --git a/tests/support/testmon_staged_changes.py b/tests/support/testmon_staged_changes.py new file mode 100644 index 00000000..26835542 --- /dev/null +++ b/tests/support/testmon_staged_changes.py @@ -0,0 +1,55 @@ +"""Make pytest-testmon see *staged* changes (required for the git pre-commit hook). + +testmon decides which files changed by calling ``git ls-files --stage -m``, which +compares the worktree against the **index**. A git pre-commit hook runs after +``git add``, so the changes are already staged and worktree == index. That makes +``git ls-files -m`` empty, testmon thinks nothing changed, and it silently +deselects every test (a false green). + +We replace the detection with ``git diff HEAD`` (staged + unstaged vs HEAD), which +is exactly the set of changes a commit is about to capture. Files in that set are +removed from testmon's git-blob-sha cache so it reads their working-tree content +instead, producing an fsha that differs from the previously recorded baseline and +therefore gets selected as changed. + +This module applies the patch at import time; it is imported from the repo-root +conftest.py so the patch is in place before testmon's ``pytest_configure`` runs. +It is a no-op (safe) when testmon is not installed. +""" + +from __future__ import annotations + +import subprocess + +try: + import testmon.process_code as _pc +except ImportError: # pragma: no cover - testmon is a dev dependency + _pc = None + +_ORIG = _pc.noncached_get_files_shas if _pc is not None else None + + +def _changed_vs_head() -> set[str]: + try: + result = subprocess.run( + ["git", "diff", "--name-only", "HEAD"], + capture_output=True, + text=True, + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return set() + return {line for line in result.stdout.splitlines() if line} + + +def _staged_aware_get_files_shas(directory: str) -> dict: + all_shas = _ORIG(directory) + # Force a working-tree read for any file that differs from HEAD so its fsha + # no longer matches the baseline recorded from the prior (committed) content. + for changed in _changed_vs_head(): + all_shas.pop(changed, None) + return all_shas + + +if _pc is not None: + _pc.noncached_get_files_shas = _staged_aware_get_files_shas diff --git a/tests/unit/agents/test_agent_manager.py b/tests/unit/agents/test_agent_manager.py index 64a1bb03..07c60952 100644 --- a/tests/unit/agents/test_agent_manager.py +++ b/tests/unit/agents/test_agent_manager.py @@ -206,14 +206,19 @@ def test_build_harness_config_includes_cronjob_tools_when_cron_manager_set( "cronjob_update", "cronjob_delete", "cronjob_run_now", + "search_knowledge", } -def test_build_harness_config_without_cron_manager_has_no_extra_tools( +def test_build_harness_config_includes_search_knowledge_without_cron( manager: AgentManager, ) -> None: + from octop.infra.knowledge.hint import KnowledgeSearchHintMiddleware + cfg = manager._build_harness_config(_row(agent_id="AGT001")) - assert cfg.tools is None + assert cfg.tools is not None + assert {t.name for t in cfg.tools} == {"search_knowledge"} + assert any(isinstance(item, KnowledgeSearchHintMiddleware) for item in (cfg.middleware or [])) def test_build_harness_config_defaults_local_shell_backend(manager: AgentManager) -> None: @@ -924,7 +929,9 @@ async def test_update_config_json_still_schedules_reload( @pytest.mark.asyncio -async def test_reload_agent_does_not_block_event_loop(tmp_path: Path) -> None: +async def test_reload_agent_does_not_block_event_loop( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: """Harness create/remove must run off the event loop (MCP init blocks the loop).""" import asyncio import time @@ -942,28 +949,42 @@ async def test_reload_agent_does_not_block_event_loop(tmp_path: Path) -> None: fake_hm = MagicMock() fake_entry = MagicMock() fake_entry.agent = MagicMock() + fake_hm.shared_factory = object() + fake_hm.acreate_agent = AsyncMock(return_value=fake_entry) + + rebuild_started = asyncio.Event() + rebuild_finished = asyncio.Event() + post_start_finished = asyncio.Event() async def slow_rebuild(*_args: object, **_kwargs: object) -> MagicMock: + rebuild_started.set() await asyncio.to_thread(time.sleep, 0.15) + rebuild_finished.set() return fake_entry + async def fake_post_start(*_args: object, **_kwargs: object) -> None: + post_start_finished.set() + fake_hm.arebuild_agent = AsyncMock(side_effect=slow_rebuild) fake_hm.aremove_agent = AsyncMock() registry._harness_manager = fake_hm + monkeypatch.setattr(registry, "_post_start_agent", fake_post_start) row = await registry.create(AgentCreateSpec(name="block-test")) + post_start_finished.clear() tick = asyncio.Event() async def ticker() -> None: - await asyncio.sleep(0.05) + await rebuild_started.wait() tick.set() - asyncio.create_task(ticker()) + ticker_task = asyncio.create_task(ticker()) await registry.update(row.agent_id, name="block-test-v2") - await asyncio.wait_for(tick.wait(), timeout=0.2) - # Background reload may still be running; wait for it to finish. - await asyncio.sleep(0.3) + await asyncio.wait_for(tick.wait(), timeout=1.0) + assert not rebuild_finished.is_set() + await asyncio.wait_for(post_start_finished.wait(), timeout=1.0) + await ticker_task def test_build_mcp_configs_registers_gateway_without_transport(manager: AgentManager) -> None: diff --git a/tests/unit/agents/test_agent_registry.py b/tests/unit/agents/test_agent_registry.py index d162a0b9..8cc29826 100644 --- a/tests/unit/agents/test_agent_registry.py +++ b/tests/unit/agents/test_agent_registry.py @@ -219,9 +219,14 @@ async def test_create_registers_with_harness(tmp_path: Path) -> None: fake_hm = _make_fake_hm() registry = _make_registry(services, fake_hm=fake_hm) - await registry.create(AgentCreateSpec(name="bot")) + row = await registry.create(AgentCreateSpec(name="bot")) fake_hm.acreate_agent.assert_called_once() + manager_skill = await registry.get_agent(row.agent_id).workspace.aread_text( + "_builtin_skills/skill-manager/SKILL.md" + ) + assert manager_skill is not None + assert "name: skill-manager" in manager_skill @pytest.mark.asyncio @@ -550,6 +555,41 @@ async def test_list_subagent_summaries_delegates_to_harness(tmp_path: Path) -> N fake_agent.list_subagent_summaries.assert_called_once_with() +@pytest.mark.asyncio +async def test_list_subagent_summaries_fills_color_from_frontmatter(tmp_path: Path) -> None: + """When harness omits color, Octop copies it from workspace frontmatter.""" + services = _make_services(tmp_path) + fake_agent = MagicMock(name="HarnessAgent") + fake_agent.list_subagent_summaries = AsyncMock( + return_value=[ + { + "slug": "helper", + "name": "Helper", + "description": "Helps", + "path": "agents/helper.md", + } + ] + ) + + async def aread_text(path: str) -> str: + assert path == "agents/helper.md" + return '---\nname: Helper\ndescription: Helps\ncolor: "#4B74FA"\n---\nBody\n' + + fake_agent.workspace.aread_text = aread_text + fake_hm = _make_fake_hm(fake_agent=fake_agent) + registry = _make_registry(services, fake_hm=fake_hm) + + agent_id = "SUBCOLOR1" + services.repos.agent_repo.create(agent_id=agent_id, user_id=None, name="live") + fake_hm.create_agent(MagicMock(), agent_id=agent_id) + + summaries = await registry.list_subagent_summaries(agent_id) + + assert len(summaries) == 1 + assert summaries[0]["slug"] == "helper" + assert summaries[0]["color"] == "#4B74FA" + + # --------------------------------------------------------------------------- # get_row() / list_rows() / get_row_by_alias() # --------------------------------------------------------------------------- diff --git a/tests/unit/agents/test_expert_catalog.py b/tests/unit/agents/test_expert_catalog.py index 89df053f..ef134d08 100644 --- a/tests/unit/agents/test_expert_catalog.py +++ b/tests/unit/agents/test_expert_catalog.py @@ -98,6 +98,12 @@ def test_preview_file_paths_limits_dashboard_preview(tmp_path: Path) -> None: skill = expert_dir / "skills" / "my-skill" / "SKILL.md" skill.parent.mkdir(parents=True) skill.write_text("# Skill", encoding="utf-8") + agents_dir = expert_dir / "agents" + agents_dir.mkdir() + (agents_dir / "reviewer.md").write_text( + "---\nname: Reviewer\ndescription: Code review\n---\n\n# Reviewer\n", + encoding="utf-8", + ) catalog = ExpertCatalog(tmp_path) catalog.refresh() @@ -105,9 +111,23 @@ def test_preview_file_paths_limits_dashboard_preview(tmp_path: Path) -> None: expert = catalog.get("my-expert") assert expert is not None preview = preview_file_paths(expert) - assert preview == ["SOUL.md", "skills/my-skill/SKILL.md"] + assert preview == [ + "SOUL.md", + "skills/my-skill/SKILL.md", + "agents/reviewer.md", + ] names = {item["name"] for item in catalog.read_file_contents("my-expert", paths=preview)} - assert names == {"SOUL.md", "skills/my-skill/SKILL.md"} + assert names == {"SOUL.md", "skills/my-skill/SKILL.md", "agents/reviewer.md"} + + +def test_preview_paths_skip_manifest_json_in_prompt_files(tmp_path: Path) -> None: + from octop.infra.agents.experts.catalog import preview_paths_from_inventory + + paths = preview_paths_from_inventory( + ["SOUL.md", "manifest.json"], + ["skills/demo/SKILL.md"], + ) + assert paths == ["SOUL.md", "skills/demo/SKILL.md"] def test_expert_prompt_files_metadata_only(tmp_path: Path) -> None: diff --git a/tests/unit/agents/test_expert_publish_snapshot.py b/tests/unit/agents/test_expert_publish_snapshot.py new file mode 100644 index 00000000..ee49355d --- /dev/null +++ b/tests/unit/agents/test_expert_publish_snapshot.py @@ -0,0 +1,390 @@ +"""Tests for exported published-expert snapshots.""" + +from __future__ import annotations + +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest +from deepagents.backends.local_shell import LocalShellBackend +from harness_agent.backends.workspace import BackendWorkspace + +from octop.infra.agents.builtin_skills import OCTOP_BUILTIN_SKILLS_ROOT +from octop.infra.agents.experts.catalog import MANIFEST_FILENAME, seed_expert_directory +from octop.infra.agents.experts.publish import ( + PublishedExpertSnapshotMeta, + assert_can_mutate_published, + export_agent_workspace_to_dir, + resolve_published_expert_slug, +) +from octop.infra.db.repos.published_experts import PublishedExpertRow +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.identity import Role, User +from octop.infra.utils.paths import PathLayout + + +def _workspace(root: Path) -> BackendWorkspace: + return BackendWorkspace(LocalShellBackend(root_dir=str(root), virtual_mode=False), root) + + +def test_published_experts_dir_is_under_instance_root(tmp_path: Path) -> None: + assert PathLayout(tmp_path).published_experts_dir == tmp_path / "published_experts" + + +def test_only_creator_or_admin_can_mutate_published_expert() -> None: + row = PublishedExpertRow( + id="01", + slug="researcher", + name="Researcher", + description="", + created_by="7", + source_agent_id=None, + icon_name="", + color="", + created_at=0, + updated_at=0, + ) + owner = User(id=7, username="owner", role=Role.USER, display_name=None) + admin = User(id=8, username="admin", role=Role.ADMIN, display_name=None) + other = User(id=9, username="other", role=Role.USER, display_name=None) + + assert_can_mutate_published(row, owner) + assert_can_mutate_published(row, admin) + with pytest.raises(OctopError) as exc: + assert_can_mutate_published(row, other) + assert exc.value.code is ErrorCode.FORBIDDEN + + +def test_resolve_published_expert_slug_retries_derived_name_and_rejects_taken_explicit_slug() -> ( + None +): + class Repo: + def __init__(self) -> None: + self.slugs = {"source-expert", "chosen-slug"} + + def get_by_slug(self, slug: str) -> object | None: + return object() if slug in self.slugs else None + + repo = Repo() + + assert resolve_published_expert_slug(repo=repo, name="Source Expert") == "source-expert-2" + with pytest.raises(OctopError) as exc: + resolve_published_expert_slug(repo=repo, name="Other", slug="chosen-slug") + assert exc.value.code is ErrorCode.PUBLISHED_EXPERT_SLUG_TAKEN + + +@pytest.mark.asyncio +async def test_export_snapshot_writes_manifest_and_seed_files(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + manifest = { + "id": "source-agent", + "label": {"zh": "来源专家", "en": "Source expert"}, + "description": {"zh": "说明", "en": "Description"}, + "quick_prompts": [{"title": {"zh": "开始", "en": "Start"}}], + } + await source.aupload_many( + [ + (MANIFEST_FILENAME, json.dumps(manifest, ensure_ascii=False).encode()), + ("SOUL.md", b"# Source soul"), + ("MEMORY.md", b"# Shared memory"), + ("skills/research/SKILL.md", b"# Research"), + (".env", b"API_KEY=secret"), + ("inbound/note.txt", b"user upload"), + ("daily/2026-01-01.md", b"journal"), + ("uploads/tmp.bin", b"binary"), + ] + ) + + destination = tmp_path / "published" + exported = await export_agent_workspace_to_dir(workspace=source, dest=destination) + + assert set(exported) == { + MANIFEST_FILENAME, + "SOUL.md", + "skills/research/SKILL.md", + } + assert json.loads((destination / MANIFEST_FILENAME).read_text(encoding="utf-8")) == manifest + assert (destination / "SOUL.md").read_text(encoding="utf-8") == "# Source soul" + assert not (destination / "MEMORY.md").exists() + assert (destination / "skills" / "research" / "SKILL.md").read_text( + encoding="utf-8" + ) == "# Research" + assert not (destination / ".env").exists() + assert not (destination / "inbound").exists() + assert not (destination / "daily").exists() + assert not (destination / "uploads").exists() + + installed_dir = tmp_path / "installed" + installed_dir.mkdir() + copied = await seed_expert_directory( + expert_dir=destination, + workspace=_workspace(installed_dir), + ) + + assert copied == 3 + assert (installed_dir / MANIFEST_FILENAME).read_text(encoding="utf-8") == json.dumps( + manifest, + ensure_ascii=False, + ) + assert (installed_dir / "skills" / "research" / "SKILL.md").read_bytes() == b"# Research" + assert not (installed_dir / "MEMORY.md").exists() + + +@pytest.mark.asyncio +async def test_export_snapshot_rejects_invalid_manifest(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + await source.aupload_many([(MANIFEST_FILENAME, b"not json")]) + + with pytest.raises(ValueError, match="valid JSON object"): + await export_agent_workspace_to_dir(workspace=source, dest=tmp_path / "published") + + +@pytest.mark.asyncio +async def test_export_snapshot_keeps_existing_directory_when_export_fails( + tmp_path: Path, +) -> None: + class FailingWorkspace: + async def adownload_bytes(self, path: str) -> bytes: + if path == MANIFEST_FILENAME: + return b"{}" + raise RuntimeError("workspace download failed") + + async def aglob(self, _pattern: str) -> SimpleNamespace: + return SimpleNamespace(matches=[{"path": "SOUL.md", "is_dir": False}]) + + destination = tmp_path / "published" + destination.mkdir() + (destination / "existing.txt").write_text("keep me", encoding="utf-8") + + with pytest.raises(RuntimeError, match="workspace download failed"): + await export_agent_workspace_to_dir( + workspace=FailingWorkspace(), # type: ignore[arg-type] + dest=destination, + ) + + assert [path.name for path in destination.iterdir()] == ["existing.txt"] + assert (destination / "existing.txt").read_text(encoding="utf-8") == "keep me" + + +@pytest.mark.asyncio +async def test_export_snapshot_excludes_runtime_workspace_dirs(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + manifest = { + "id": "source-agent", + "label": {"zh": "来源专家", "en": "Source expert"}, + "description": {"zh": "说明", "en": "Description"}, + } + await source.aupload_many( + [ + (MANIFEST_FILENAME, json.dumps(manifest, ensure_ascii=False).encode()), + ("SOUL.md", b"# Source soul"), + (f"{OCTOP_BUILTIN_SKILLS_ROOT}/skill-manager/SKILL.md", b"# Builtin"), + ("logs/agent.log", b"runtime log line"), + ] + ) + + destination = tmp_path / "published" + exported = await export_agent_workspace_to_dir( + workspace=source, + dest=destination, + metadata=PublishedExpertSnapshotMeta( + name="Published Expert", + description="Published description", + icon_name=None, + color=None, + label_zh="发布专家", + label_en="Published Expert", + welcome_message_zh="", + welcome_message_en="", + ), + manifest_id="published-expert", + ) + + assert set(exported) == {MANIFEST_FILENAME, "SOUL.md"} + assert not (destination / OCTOP_BUILTIN_SKILLS_ROOT).exists() + assert not (destination / "logs").exists() + merged = json.loads((destination / MANIFEST_FILENAME).read_text(encoding="utf-8")) + assert merged["id"] == "published-expert" + assert merged["label"] == {"zh": "发布专家", "en": "Published Expert"} + assert merged["description"] == { + "zh": "Published description", + "en": "Published description", + } + + +@pytest.mark.asyncio +async def test_export_snapshot_excludes_memory_sqlite_files(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + await source.aupload_many( + [ + ("SOUL.md", b"# Source soul"), + ("memory.sqlite", b"sqlite"), + ("memory.sqlite-wal", b"wal"), + ("memory.sqlite-shm", b"shm"), + ] + ) + + destination = tmp_path / "published" + exported = await export_agent_workspace_to_dir( + workspace=source, + dest=destination, + metadata=PublishedExpertSnapshotMeta( + name="Expert", + description="desc", + icon_name=None, + color=None, + label_zh="专家", + label_en="Expert", + welcome_message_zh="欢迎", + welcome_message_en="Welcome", + ), + manifest_id="expert", + ) + + assert set(exported) == {MANIFEST_FILENAME, "SOUL.md"} + assert not (destination / "memory.sqlite").exists() + + +@pytest.mark.asyncio +async def test_export_snapshot_builds_manifest_from_publish_metadata_only( + tmp_path: Path, +) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + manifest = { + "id": "general-assistant", + "label": {"zh": "旧名称", "en": "Old name"}, + "description": {"zh": "旧说明", "en": "Old description"}, + "welcome_message": {"zh": "欢迎回来", "en": "Welcome back"}, + "quick_prompts": [{"title": {"zh": "开始", "en": "Start"}}], + "icon_name": "zap", + "color": "#111111", + "skillhub": {"manifest_generated": {"by": "test"}}, + } + await source.aupload_many( + [ + (MANIFEST_FILENAME, json.dumps(manifest, ensure_ascii=False).encode()), + ("SOUL.md", b"# Source soul"), + ] + ) + + destination = tmp_path / "published-expert" + await export_agent_workspace_to_dir( + workspace=source, + dest=destination, + metadata=PublishedExpertSnapshotMeta( + name="Research Expert", + description="Helps with research", + icon_name="search", + color="#123456", + label_zh="研究专家", + label_en="Research Expert", + welcome_message_zh="开始研究吧", + welcome_message_en="Start researching", + ), + manifest_id="research-expert", + ) + + merged = json.loads((destination / MANIFEST_FILENAME).read_text(encoding="utf-8")) + assert merged["id"] == "research-expert" + assert merged["label"] == {"zh": "研究专家", "en": "Research Expert"} + assert merged["description"] == { + "zh": "Helps with research", + "en": "Helps with research", + } + assert merged["welcome_message"] == { + "zh": "开始研究吧", + "en": "Start researching", + } + assert "quick_prompts" not in merged + assert "skillhub" not in merged + assert merged["icon_name"] == "search" + assert merged["color"] == "#123456" + assert merged["prompt_files"] == ["SOUL.md"] + + +@pytest.mark.asyncio +async def test_export_snapshot_includes_subagent_definitions(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + await source.aupload_many( + [ + ("SOUL.md", b"# Soul"), + ("references/extra.json", b"{}"), + ("agents/reviewer.md", b"# Reviewer"), + ("skills/demo/SKILL.md", b"# Skill"), + ] + ) + + destination = tmp_path / "published" + exported = await export_agent_workspace_to_dir( + workspace=source, + dest=destination, + metadata=PublishedExpertSnapshotMeta( + name="Expert", + description="desc", + icon_name=None, + color=None, + label_zh="专家", + label_en="Expert", + welcome_message_zh="", + welcome_message_en="", + ), + manifest_id="expert", + ) + + assert set(exported) == { + MANIFEST_FILENAME, + "SOUL.md", + "agents/reviewer.md", + "skills/demo/SKILL.md", + } + assert not (destination / "references").exists() + + +@pytest.mark.asyncio +async def test_export_snapshot_builds_manifest_from_publish_metadata(tmp_path: Path) -> None: + source_dir = tmp_path / "source" + source_dir.mkdir() + source = _workspace(source_dir) + await source.aupload_many([("SOUL.md", b"# Source soul")]) + metadata = PublishedExpertSnapshotMeta( + name="Research Expert", + description="Helps with research", + icon_name="search", + color="#123456", + label_zh="研究专家", + label_en="Research Expert", + welcome_message_zh="开始研究", + welcome_message_en="Start researching", + ) + + destination = tmp_path / "research-expert" + exported = await export_agent_workspace_to_dir( + workspace=source, + dest=destination, + metadata=metadata, + manifest_id="research-expert", + ) + + assert set(exported) == {MANIFEST_FILENAME, "SOUL.md"} + assert json.loads((destination / MANIFEST_FILENAME).read_text(encoding="utf-8")) == { + "id": "research-expert", + "label": {"zh": "研究专家", "en": "Research Expert"}, + "description": {"zh": "Helps with research", "en": "Helps with research"}, + "welcome_message": {"zh": "开始研究", "en": "Start researching"}, + "icon_name": "search", + "color": "#123456", + "prompt_files": ["SOUL.md"], + } diff --git a/tests/unit/agents/test_model_flags.py b/tests/unit/agents/test_model_flags.py new file mode 100644 index 00000000..eac5f175 --- /dev/null +++ b/tests/unit/agents/test_model_flags.py @@ -0,0 +1,110 @@ +"""Tests for embedding / chat-eligibility model flags.""" + +from __future__ import annotations + +from types import SimpleNamespace + +from octop.infra.agents.providers.model_flags import ( + is_chat_eligible_model, + is_embedding_model, +) +from octop.infra.agents.providers.resolved import list_resolved_models + + +def test_embedding_flag_and_task() -> None: + assert is_embedding_model({"id": "a", "embedding": True}) + assert is_embedding_model({"id": "a", "task": "embedding"}) + assert not is_embedding_model({"id": "a", "enabled": True}) + + +def test_onnx_provider_identity_marks_models_embedding() -> None: + assert is_embedding_model( + {"id": "BAAI/bge-small-zh-v1.5", "enabled": True}, + provider_name="ONNX (Local)", + provider_api_key="onnx", + ) + assert is_embedding_model( + {"id": "BAAI/bge-small-zh-v1.5", "enabled": True}, + provider_name="ONNX (Local)", + ) + assert not is_embedding_model( + {"id": "gpt-4o", "enabled": True}, + provider_name="My ONNX Cloud Proxy", + ) + assert not is_embedding_model( + {"id": "gpt-4o", "enabled": True}, + provider_name="OpenAI", + ) + + +def test_is_onnx_local_provider() -> None: + from octop.infra.agents.providers.model_flags import is_onnx_local_provider + + assert is_onnx_local_provider(provider_api_key="onnx") + assert is_onnx_local_provider(provider_name="ONNX (Local)") + assert is_onnx_local_provider(provider_name="onnx") + # Ollama is also local — must not match via "local" substring heuristics. + assert not is_onnx_local_provider(provider_name="Ollama (Local)") + assert not is_onnx_local_provider(provider_name="My ONNX Cloud Proxy") + assert not is_onnx_local_provider(provider_name="OpenAI") + assert not is_onnx_local_provider(provider_name=None) + + +def test_is_local_runtime_provider() -> None: + from octop.infra.agents.providers.model_flags import is_local_runtime_provider + + assert is_local_runtime_provider(provider_api_key="onnx") + assert is_local_runtime_provider(provider_name="ONNX (Local)") + assert is_local_runtime_provider(provider_api_key="ollama") + assert is_local_runtime_provider(provider_name="Ollama (Local)") + assert is_local_runtime_provider(provider_base_url="http://127.0.0.1:11434") + assert not is_local_runtime_provider( + provider_name="OpenAI", + provider_api_key="sk-x", + provider_base_url="https://api.openai.com/v1", + ) + assert not is_local_runtime_provider( + provider_name="My ONNX Cloud Proxy", + provider_api_key="sk-x", + provider_base_url="https://proxy.example/v1", + ) + + +def test_chat_eligible_excludes_embedding() -> None: + assert is_chat_eligible_model({"id": "gpt", "enabled": True}) + assert not is_chat_eligible_model({"id": "emb", "enabled": True, "embedding": True}) + assert not is_chat_eligible_model({"id": "gpt", "enabled": False}) + + +def test_list_resolved_models_skips_embedding() -> None: + providers = [ + SimpleNamespace( + id=1, + name="ONNX (Local)", + kind="openai", + enabled=True, + api_key="onnx", + base_url="", + get_models=lambda: [ + { + "id": "BAAI/bge-small-zh-v1.5", + "name": "bge", + "enabled": True, + "embedding": True, + } + ], + ), + SimpleNamespace( + id=2, + name="OpenAI", + kind="openai", + enabled=True, + api_key="sk-x", + base_url="https://api.openai.com/v1", + get_models=lambda: [ + {"id": "gpt-4o", "name": "GPT-4o", "enabled": True}, + ], + ), + ] + resolved = list_resolved_models(providers) + assert [r["model"] for r in resolved] == ["gpt-4o"] diff --git a/tests/unit/agents/test_octop_builtin_skills.py b/tests/unit/agents/test_octop_builtin_skills.py new file mode 100644 index 00000000..5488d7a6 --- /dev/null +++ b/tests/unit/agents/test_octop_builtin_skills.py @@ -0,0 +1,225 @@ +"""Tests for Octop-owned built-in Skills and the Skill Manager helper.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import zipfile +from importlib import resources +from pathlib import Path + +import pytest +from tests.support.fakes import FakeHarnessAgent + +from octop.infra.agents.builtin_skills import sync_octop_builtin_skills + +_PACKAGE = "octop.infra.agents.builtin_skills" + + +def _manager_script() -> Path: + script = ( + resources.files(_PACKAGE) + .joinpath("skill-manager") + .joinpath("scripts") + .joinpath("manage_skills.py") + ) + return Path(str(script)) + + +def _run_manager(workspace: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(_manager_script()), "--workspace", str(workspace), *args], + check=False, + capture_output=True, + text=True, + ) + + +@pytest.mark.asyncio +async def test_sync_seeds_manager_and_retires_old_installer(tmp_path: Path) -> None: + agent = FakeHarnessAgent(workspace_dir=tmp_path, virtual_mode=False) + await agent.workspace.awrite_text( + "_builtin_skills/install-skill/SKILL.md", + "---\nname: install-skill\ndescription: old\n---\n", + force=True, + ) + + synced = await sync_octop_builtin_skills(agent.workspace) + + assert synced == ["skill-manager"] + assert await agent.workspace.aexists("skills") + assert await agent.workspace.aread_text("_builtin_skills/install-skill/SKILL.md") is None + manager = await agent.workspace.aread_text("_builtin_skills/skill-manager/SKILL.md") + script = await agent.workspace.aread_text( + "_builtin_skills/skill-manager/scripts/manage_skills.py" + ) + assert manager is not None and "name: skill-manager" in manager + assert str(tmp_path) in manager + assert "{{OCTOP_WORKSPACE}}" not in manager + assert script is not None and "def main()" in script + + deployed = tmp_path / "_builtin_skills/skill-manager/scripts/manage_skills.py" + inferred = subprocess.run( + [sys.executable, str(deployed), "list"], + check=False, + capture_output=True, + text=True, + ) + assert inferred.returncode == 0, inferred.stderr or inferred.stdout + assert json.loads(inferred.stdout) == [] + + +def test_manager_script_compiles() -> None: + script = _manager_script() + assert script.is_file() + compile(script.read_text(encoding="utf-8"), "manage_skills.py", "exec") + + +def test_manager_installs_lists_removes_and_restores_zip(tmp_path: Path) -> None: + archive = tmp_path / "example.zip" + with zipfile.ZipFile(archive, "w") as bundle: + bundle.writestr( + "example/SKILL.md", + "---\nname: example\ndescription: Example test skill.\n---\n\n# Example\n", + ) + bundle.writestr("example/references/note.md", "hello\n") + + inspected = _run_manager(tmp_path, "inspect", str(archive)) + assert inspected.returncode == 0, inspected.stderr or inspected.stdout + assert json.loads(inspected.stdout)[0]["slug"] == "example" + + installed = _run_manager(tmp_path, "install", str(archive)) + assert installed.returncode == 0, installed.stderr or installed.stdout + assert (tmp_path / "skills" / "example" / "references" / "note.md").is_file() + + listed = _run_manager(tmp_path, "list") + assert listed.returncode == 0, listed.stderr or listed.stdout + assert json.loads(listed.stdout)[0]["valid"] is True + + unconfirmed = _run_manager(tmp_path, "remove", "example") + assert unconfirmed.returncode == 1 + assert (tmp_path / "skills" / "example").is_dir() + + removed = _run_manager(tmp_path, "remove", "example", "--yes") + assert removed.returncode == 0, removed.stderr or removed.stdout + trash_name = json.loads(removed.stdout)["trash_name"] + assert not (tmp_path / "skills" / "example").exists() + + restored = _run_manager(tmp_path, "restore", trash_name) + assert restored.returncode == 0, restored.stderr or restored.stdout + assert (tmp_path / "skills" / "example" / "SKILL.md").is_file() + assert not (tmp_path / "skills" / "example" / ".skill-manager-trash.json").exists() + + +def test_manager_rejects_archive_traversal(tmp_path: Path) -> None: + archive = tmp_path / "unsafe.zip" + with zipfile.ZipFile(archive, "w") as bundle: + bundle.writestr("../SKILL.md", "not safe") + + result = _run_manager(tmp_path, "inspect", str(archive)) + + assert result.returncode == 1 + assert "unsafe archive path" in result.stdout + assert not (tmp_path.parent / "SKILL.md").exists() + + +def test_manager_refuses_overwrite_without_force(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "SKILL.md").write_text( + "---\nname: stable\ndescription: First version.\n---\n\n# Stable\n", + encoding="utf-8", + ) + first = _run_manager(tmp_path, "install", str(source)) + assert first.returncode == 0, first.stderr or first.stdout + + (source / "SKILL.md").write_text( + "---\nname: stable\ndescription: Second version.\n---\n\n# Stable\n", + encoding="utf-8", + ) + refused = _run_manager(tmp_path, "install", str(source)) + assert refused.returncode == 1 + assert "ask before using --force" in refused.stdout + assert "First version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text() + + replaced = _run_manager(tmp_path, "install", str(source), "--force") + assert replaced.returncode == 0, replaced.stderr or replaced.stdout + assert "Second version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text() + + +def test_manager_refuses_to_replace_itself(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "SKILL.md").write_text( + "---\nname: skill-manager\ndescription: Untrusted replacement.\n---\n", + encoding="utf-8", + ) + + result = _run_manager(tmp_path, "install", str(source), "--force") + + assert result.returncode == 1 + assert "Octop-owned built-in" in result.stdout + assert not (tmp_path / "skills" / "skill-manager").exists() + + +def test_manager_installs_namespaced_skillhub_page_url(tmp_path: Path) -> None: + fake_bin = tmp_path / "fake-bin" + fake_bin.mkdir() + log_path = tmp_path / "skillhub-args.json" + fake_skillhub = fake_bin / ("skillhub.py" if os.name == "nt" else "skillhub") + fake_skillhub.write_text( + """#!/usr/bin/env python3 +import json +import os +import sys +from pathlib import Path + +args = sys.argv[1:] +Path(os.environ["FAKE_SKILLHUB_LOG"]).write_text(json.dumps(args), encoding="utf-8") +slug = args[args.index("install") + 1] +namespace = args[args.index("--namespace") + 1] +root = Path(args[args.index("--dir") + 1]) / f"@{namespace}" / slug +root.mkdir(parents=True) +(root / "SKILL.md").write_text( + f"---\\nname: {slug}\\ndescription: Namespaced SkillHub test.\\n---\\n", + encoding="utf-8", +) +print(json.dumps({"installed": slug})) +""", + encoding="utf-8", + ) + if os.name == "nt": + (fake_bin / "skillhub.cmd").write_text( + f'@"{sys.executable}" "%~dp0skillhub.py" %*\n', + encoding="utf-8", + ) + else: + fake_skillhub.chmod(0o755) + env = { + **os.environ, + "PATH": f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}", + "FAKE_SKILLHUB_LOG": str(log_path), + } + + result = subprocess.run( + [ + sys.executable, + str(_manager_script()), + "--workspace", + str(tmp_path), + "install", + "https://skillhub.cn/skills/user_741dc82b/dev-expert?from=skill-hunt", + ], + check=False, + capture_output=True, + text=True, + env=env, + ) + + assert result.returncode == 0, result.stderr or result.stdout + assert (tmp_path / "skills/dev-expert/SKILL.md").is_file() + args = json.loads(log_path.read_text(encoding="utf-8")) + assert args[args.index("install") + 1] == "dev-expert" + assert args[args.index("--namespace") + 1] == "user_741dc82b" diff --git a/tests/unit/agents/test_onnx_service.py b/tests/unit/agents/test_onnx_service.py new file mode 100644 index 00000000..30ac15c2 --- /dev/null +++ b/tests/unit/agents/test_onnx_service.py @@ -0,0 +1,272 @@ +"""Tests for local ONNX embedding service helpers.""" + +from __future__ import annotations + +import json + +import pytest + +from octop.infra.agents.providers.onnx_catalog import ( + ONNX_PRESET_MODEL_IDS, + get_onnx_model_meta, + list_onnx_catalog_models, +) +from octop.infra.agents.providers.onnx_service import ( + OnnxDownloadState, + OnnxServiceConfig, + embedding_models_dir, + is_model_downloaded, + load_config, + local_embedding_deps_available, + save_config, + status_payload, +) + + +def test_catalog_includes_finnie_presets() -> None: + ids = [m["id"] for m in list_onnx_catalog_models()] + for mid in ONNX_PRESET_MODEL_IDS: + assert mid in ids + recommended = {m["id"] for m in list_onnx_catalog_models() if m.get("recommended")} + assert set(ONNX_PRESET_MODEL_IDS) <= recommended + + +def test_catalog_models_have_approximate_sizes() -> None: + for model_id in ( + *ONNX_PRESET_MODEL_IDS, + "thenlper/gte-base", + "thenlper/gte-large", + "jinaai/jina-embeddings-v2-base-en", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + ): + size = get_onnx_model_meta(model_id).get("size_gb") + assert size is not None, model_id + assert float(size) > 0 + + +def test_config_roundtrip_via_settings_dict(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + store: dict[str, str] = {} + + def getter(key: str) -> str | None: + return store.get(key) + + def setter(key: str, value: str) -> None: + store[key] = value + + cfg = OnnxServiceConfig(enabled=True, model=ONNX_PRESET_MODEL_IDS[1]) + save_config(setter, cfg) + loaded = load_config(getter) + assert loaded.enabled is True + assert loaded.model == ONNX_PRESET_MODEL_IDS[1] + assert json.loads(store["onnx_local_service"])["model"] == ONNX_PRESET_MODEL_IDS[1] + + payload = status_payload(getter, OnnxDownloadState()) + assert payload["enabled"] is True + assert payload["ready"] is False + assert "deps_available" in payload + assert payload["deps_available"] is local_embedding_deps_available() + assert embedding_models_dir() == tmp_path / "embedding_models" + assert is_model_downloaded("no/such-model") is False + + +def test_ensure_deps_noop_when_available(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + + def boom(*_args, **_kwargs) -> str: + raise AssertionError("should not install packages") + + monkeypatch.setattr(mod, "install_packages", boom) + assert mod.ensure_local_embedding_deps() == "ready" + + +def test_ensure_deps_refuses_install_by_default(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.delenv("OCTOP_ALLOW_RUNTIME_PIP", raising=False) + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: False) + with pytest.raises(RuntimeError, match="Enable the ONNX service"): + mod.ensure_local_embedding_deps() + + +def test_ensure_deps_installs_when_env_allows(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.setenv("OCTOP_ALLOW_RUNTIME_PIP", "1") + state = {"ok": False} + + def available() -> bool: + return state["ok"] + + def fake_install(*_args, **_kwargs) -> str: + state["ok"] = True + return "installed" + + monkeypatch.setattr(mod, "local_embedding_deps_available", available) + monkeypatch.setattr(mod, "install_packages", fake_install) + assert mod.ensure_local_embedding_deps() == "installed" + assert state["ok"] is True + + +def test_ensure_deps_installs_when_allow_install_true(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.delenv("OCTOP_ALLOW_RUNTIME_PIP", raising=False) + state = {"ok": False} + + def available() -> bool: + return state["ok"] + + def fake_install(*_args, **_kwargs) -> str: + state["ok"] = True + return "installed" + + monkeypatch.setattr(mod, "local_embedding_deps_available", available) + monkeypatch.setattr(mod, "install_packages", fake_install) + assert mod.ensure_local_embedding_deps(allow_install=True) == "installed" + assert state["ok"] is True + + +def test_status_payload_omits_install_commands(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + store: dict[str, str] = {} + + payload = status_payload(store.get, OnnxDownloadState()) + assert "deps_install_hint" not in payload + + +def test_assert_catalog_rejects_unknown_and_path_tricks() -> None: + from octop.infra.agents.providers import onnx_service as mod + + with pytest.raises(ValueError, match="catalog"): + mod.assert_catalog_model("evil/not-in-catalog") + with pytest.raises(ValueError, match="invalid"): + mod.assert_catalog_model("../etc/passwd") + with pytest.raises(ValueError, match="invalid"): + mod.assert_catalog_model("/abs/path") + assert mod.assert_catalog_model(ONNX_PRESET_MODEL_IDS[0]) == ONNX_PRESET_MODEL_IDS[0] + + +def test_partial_cache_without_onnx_is_not_downloaded(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + from octop.infra.agents.providers import onnx_service as mod + + mid = ONNX_PRESET_MODEL_IDS[0] + partial = mod.model_cache_dir(mid) + partial.mkdir(parents=True) + (partial / "config.json").write_text("{}", encoding="utf-8") + mod.mark_model_downloaded(mid) + assert mod.is_model_downloaded(mid) is False + + +def test_is_downloaded_recognizes_fastembed_hf_alias(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + from octop.infra.agents.providers import onnx_catalog as catalog + from octop.infra.agents.providers import onnx_service as mod + + # CI often lacks the optional local-embedding extra; alias detection must + # still work via static HF source fallbacks. + monkeypatch.setattr(catalog, "_fastembed_meta_map", lambda: {}) + + cache = mod.embedding_models_dir() + alias = cache / "models--Qdrant--bge-small-zh-v1.5" / "snapshots" / "abc" + alias.mkdir(parents=True) + (alias / "model.onnx").write_bytes(b"x") + assert mod.is_model_downloaded("BAAI/bge-small-zh-v1.5") is True + assert "BAAI/bge-small-zh-v1.5" in mod.list_downloaded_models() + + +def test_embedding_prerequisites_ignore_service_enabled(tmp_path, monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + store: dict[str, str] = {} + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr(mod, "is_model_downloaded", lambda _m: True) + mod.save_config( + store.__setitem__, + mod.OnnxServiceConfig(enabled=False, model=mod._DEFAULT_MODEL), + ) + assert mod.embedding_prerequisites_ok(store.get) is True + + +def test_embed_texts_returns_vectors(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + class FakeEmb: + def embed(self, texts): + for _ in texts: + yield [0.1, 0.2, 0.3] + + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr(mod, "_build_text_embedding", lambda _model: FakeEmb()) + vectors = mod.embed_texts("BAAI/bge-small-zh-v1.5", ["hello"]) + assert len(vectors) == 1 and len(vectors[0]) == 3 + + +def test_embed_texts_empty_input(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + called = {"n": 0} + + def boom(_model: str) -> None: + called["n"] += 1 + raise AssertionError("should not build embedding for empty input") + + monkeypatch.setattr(mod, "_build_text_embedding", boom) + assert mod.embed_texts("BAAI/bge-small-zh-v1.5", []) == [] + assert called["n"] == 0 + + +def test_embedding_prerequisites_ok_for_model(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr( + mod, + "is_model_downloaded", + lambda m: m == "BAAI/bge-small-zh-v1.5", + ) + assert mod.embedding_prerequisites_ok_for_model("BAAI/bge-small-zh-v1.5") is True + assert mod.embedding_prerequisites_ok_for_model("BAAI/bge-small-en-v1.5") is False + + +def test_require_embedding_prerequisites_for_model(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr(mod, "is_model_downloaded", lambda _m: True) + assert ( + mod.require_embedding_prerequisites_for_model("BAAI/bge-small-zh-v1.5") + == "BAAI/bge-small-zh-v1.5" + ) + + +def test_require_embedding_prerequisites_for_model_raises(monkeypatch) -> None: + from octop.infra.agents.providers import onnx_service as mod + + monkeypatch.setattr(mod, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr(mod, "is_model_downloaded", lambda _m: False) + with pytest.raises(RuntimeError, match="not configured or not downloaded"): + mod.require_embedding_prerequisites_for_model("BAAI/bge-small-zh-v1.5") + + +def test_delete_stays_under_cache_root(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + from octop.infra.agents.providers import onnx_service as mod + + mid = ONNX_PRESET_MODEL_IDS[0] + outside = tmp_path / "outside_secret.txt" + outside.write_text("secret", encoding="utf-8") + # Symlink attack: cache entry pointing outside should not delete outside. + cache_entry = mod.model_cache_dir(mid) + cache_entry.parent.mkdir(parents=True, exist_ok=True) + try: + cache_entry.symlink_to(outside) + except OSError: + pytest.skip("symlinks unavailable") + mod.delete_downloaded_model(mid) + assert outside.is_file() + assert outside.read_text(encoding="utf-8") == "secret" diff --git a/tests/unit/agents/test_skill_package_store.py b/tests/unit/agents/test_skill_package_store.py index 9bfcca46..d0ed706e 100644 --- a/tests/unit/agents/test_skill_package_store.py +++ b/tests/unit/agents/test_skill_package_store.py @@ -32,6 +32,7 @@ def store(tmp_path: Path, db: SqlitePool) -> SkillPackageStore: def test_create_package_makes_skills_dir(store: SkillPackageStore) -> None: row = store.create(name="P", description="", created_by="42") + assert len(row.id) == 6 assert (store.root / row.id / "skills").is_dir() diff --git a/tests/unit/agents/test_thread_fork.py b/tests/unit/agents/test_thread_fork.py new file mode 100644 index 00000000..afd57c3b --- /dev/null +++ b/tests/unit/agents/test_thread_fork.py @@ -0,0 +1,231 @@ +"""Tests for conversation fork-from-user-message.""" + +from __future__ import annotations + +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + +from octop.infra.agents.thread_fork import ( + _fork_title, + find_user_fork_index, + fork_dashboard_thread, + write_checkpoint_messages, +) +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.gateway.threads import ThreadRegistry + + +def test_fork_title_suffix() -> None: + assert _fork_title("My chat", "en") == "My chat (fork)" + assert _fork_title("My chat", "zh") == "My chat(分叉)" + assert _fork_title(None, "en") == "Forked chat" + assert _fork_title("", "zh") == "分叉对话" + + +def test_find_user_fork_index_by_suffix_count() -> None: + messages = [ + HumanMessage(content="one", id="h1"), + AIMessage(content="a1", id="a1"), + HumanMessage(content="two", id="h2"), + AIMessage(content="a2", id="a2"), + HumanMessage(content="three", id="h3"), + ] + assert find_user_fork_index(messages, message_id="x", user_turns_from_end=2) == 2 + assert find_user_fork_index(messages, message_id="x", user_turns_from_end=1) == 4 + assert find_user_fork_index(messages, message_id="x", user_turns_from_end=3) == 0 + + +def test_find_user_fork_index_by_id_when_suffix_mismatches_content() -> None: + messages = [ + HumanMessage(content="keep", id="h1"), + AIMessage(content="a1", id="a1"), + HumanMessage(content="fork-me", id="h2"), + ] + idx = find_user_fork_index( + messages, + message_id="h2", + content="other text", + user_turns_from_end=1, + ) + assert idx == 2 + + +def test_find_user_fork_index_by_content_fallback() -> None: + messages = [ + HumanMessage(content="alpha", id="h1"), + AIMessage(content="a1", id="a1"), + HumanMessage(content="beta", id="h2"), + ] + assert find_user_fork_index(messages, message_id="missing", content="beta") == 2 + + +def test_find_user_fork_index_missing_raises() -> None: + messages = [AIMessage(content="only assistant", id="a1")] + with pytest.raises(OctopError) as exc: + find_user_fork_index(messages, message_id="h1") + assert exc.value.code is ErrorCode.NOT_FOUND + + +@pytest.mark.asyncio +async def test_write_checkpoint_messages_skips_empty() -> None: + graph = SimpleNamespace(aupdate_state=AsyncMock()) + harness = SimpleNamespace(graph=graph) + await write_checkpoint_messages(harness, thread_id="thr_new", messages=[]) + graph.aupdate_state.assert_not_called() + + +@pytest.mark.asyncio +async def test_fork_dashboard_thread_copies_prefix_and_rebinds() -> None: + source_id = "thr_src" + messages = [ + HumanMessage(content="first", id="h1"), + AIMessage(content="ok", id="a1"), + ToolMessage(content="tool", id="t1", tool_call_id="c1"), + HumanMessage(content="second", id="h2"), + AIMessage(content="later", id="a2"), + ] + store: dict[str, list[Any]] = {source_id: list(messages)} + graph = SimpleNamespace() + + async def aget_state(config: dict[str, Any]) -> SimpleNamespace: + tid = config["configurable"]["thread_id"] + return SimpleNamespace(values={"messages": list(store.get(tid, []))}) + + async def aupdate_state(config: dict[str, Any], values: dict[str, Any], **_k: Any) -> None: + tid = config["configurable"]["thread_id"] + store.setdefault(tid, []).extend(list(values.get("messages") or [])) + + graph.aget_state = aget_state + graph.aupdate_state = aupdate_state + harness = SimpleNamespace(graph=graph) + + source = MagicMock( + thread_id=source_id, + agent_id="agt_1", + title="Original title", + model_ref="openai/gpt-4o", + reasoning_mode="auto", + reasoning_effort=None, + ) + created: dict[str, Any] = {} + + async def rebind(**kwargs: Any) -> None: + created["rebind"] = kwargs + + registry = MagicMock() + registry.create_thread.return_value = "thr_fork" + registry.get_thread.return_value = MagicMock( + title="Original title", + last_active=9, + created_at=8, + ) + registry.rebind = rebind + + out = await fork_dashboard_thread( + thread_registry=registry, + harness=harness, + source=source, + user_id=7, + message_id="h2", + content="second", + user_turns_from_end=1, + ) + + assert out["thread_id"] == "thr_fork" + assert out["source_thread_id"] == source_id + assert out["copied_messages"] == 3 + assert [m.id for m in store["thr_fork"]] == ["h1", "a1", "t1"] + registry.update_title.assert_called_once_with("thr_fork", "Original title (fork)") + registry.touch_last_active.assert_not_called() + registry.update_composer.assert_called_once() + assert created["rebind"]["thread_id"] == "thr_fork" + assert created["rebind"]["session_key"] == ThreadRegistry.dashboard_key( + agent_id="agt_1", user_id=7 + ) + + +@pytest.mark.asyncio +async def test_fork_dashboard_thread_from_first_question_is_empty() -> None: + source_id = "thr_src" + messages = [HumanMessage(content="only", id="h1"), AIMessage(content="a", id="a1")] + store: dict[str, list[Any]] = {source_id: list(messages)} + graph = SimpleNamespace() + + async def aget_state(config: dict[str, Any]) -> SimpleNamespace: + tid = config["configurable"]["thread_id"] + return SimpleNamespace(values={"messages": list(store.get(tid, []))}) + + graph.aget_state = aget_state + graph.aupdate_state = AsyncMock() + harness = SimpleNamespace(graph=graph) + + registry = MagicMock() + registry.create_thread.return_value = "thr_fork" + registry.get_thread.return_value = MagicMock(title=None, last_active=0, created_at=1) + registry.rebind = AsyncMock() + + out = await fork_dashboard_thread( + thread_registry=registry, + harness=harness, + source=MagicMock( + thread_id=source_id, + agent_id="agt_1", + title="only", + model_ref=None, + reasoning_mode=None, + reasoning_effort=None, + ), + user_id=1, + message_id="h1", + user_turns_from_end=1, + ) + + assert out["copied_messages"] == 0 + graph.aupdate_state.assert_not_called() + registry.update_title.assert_not_called() + registry.touch_last_active.assert_not_called() + + +@pytest.mark.asyncio +async def test_fork_dashboard_thread_deletes_row_when_write_fails() -> None: + source_id = "thr_src" + messages = [ + HumanMessage(content="first", id="h1"), + AIMessage(content="ok", id="a1"), + HumanMessage(content="second", id="h2"), + ] + graph = SimpleNamespace() + + async def aget_state(_config: dict[str, Any]) -> SimpleNamespace: + return SimpleNamespace(values={"messages": list(messages)}) + + graph.aget_state = aget_state + graph.aupdate_state = AsyncMock(side_effect=RuntimeError("checkpoint write failed")) + harness = SimpleNamespace(graph=graph) + registry = MagicMock() + registry.create_thread.return_value = "thr_fork" + registry.rebind = AsyncMock() + + with pytest.raises(RuntimeError, match="checkpoint write failed"): + await fork_dashboard_thread( + thread_registry=registry, + harness=harness, + source=MagicMock( + thread_id=source_id, + agent_id="agt_1", + title="t", + model_ref=None, + reasoning_mode=None, + reasoning_effort=None, + ), + user_id=1, + message_id="h2", + user_turns_from_end=1, + ) + + registry.delete_thread.assert_called_once_with("thr_fork") + registry.rebind.assert_not_called() diff --git a/tests/unit/api/test_acl_gate_coverage.py b/tests/unit/api/test_acl_gate_coverage.py new file mode 100644 index 00000000..62015656 --- /dev/null +++ b/tests/unit/api/test_acl_gate_coverage.py @@ -0,0 +1,82 @@ +"""ACL gate coverage: no current_admin; known permission keys only.""" + +from __future__ import annotations + +import ast +import pathlib + +from octop.infra.users.permissions import ALL_PERMISSION_KEYS + +API_ROOT = pathlib.Path("src/octop/api") + +GATED_FILES = [ + "routers/users.py", + "routers/admin.py", + "routers/backup.py", + "routers/security.py", + "routers/providers.py", + "routers/voice.py", + "routers/storage_backends.py", + "routers/envs.py", + "routers/observability.py", + "routers/tls.py", + "routers/auth_oidc.py", + "routers/update.py", + "routers/search.py", + "routers/ollama_models.py", + "routers/onnx_models.py", + "routers/connectors.py", + "routers/knowledge_bases.py", + "routers/browser/uninstall.py", + "routers/browser/sessions.py", + "routers/desktop/install.py", + "routers/desktop/uninstall.py", + "routers/desktop/status.py", + "routers/desktop/settings.py", + "routers/plugins.py", + "routers/agents.py", + "routers/channels.py", + "routers/skill_packages.py", + "routers/terminal.py", + "routers/acp.py", +] + + +def test_no_current_admin_symbol_remains() -> None: + hits: list[str] = [] + for path in API_ROOT.rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, ast.Name) and node.id == "current_admin": + hits.append(str(path)) + assert not hits, f"current_admin still referenced in: {hits}" + + +def test_gated_files_call_require_permission_with_known_keys() -> None: + for rel in GATED_FILES: + src = (API_ROOT / rel).read_text(encoding="utf-8") + assert "current_admin" not in src + assert ( + "require_permission(" in src or "user_has_permission(" in src or "require_admin(" in src + ) + tree = ast.parse(src, filename=rel) + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "require_permission" + and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str) + ): + assert node.args[0].value in ALL_PERMISSION_KEYS, ( + f"{rel}: unknown key {node.args[0].value!r}" + ) + + +def test_as_user_still_requires_admin() -> None: + """Cross-user impersonation must not become a module permission.""" + agent = (API_ROOT / "common/agent.py").read_text(encoding="utf-8") + usage = (API_ROOT / "routers/usage.py").read_text(encoding="utf-8") + assert "as_user requires admin" in agent or "is_admin" in agent + assert "as_user" in usage diff --git a/tests/unit/api/test_agent_access.py b/tests/unit/api/test_agent_access.py index ec601568..b5945730 100644 --- a/tests/unit/api/test_agent_access.py +++ b/tests/unit/api/test_agent_access.py @@ -6,7 +6,12 @@ import pytest -from octop.api.common.agent import assert_agent_owner +from octop.api.common.agent import ( + agent_is_shared, + assert_agent_access_row, + assert_agent_owner, + user_owns_agent, +) from octop.infra.errors import ErrorCode, OctopError @@ -36,3 +41,44 @@ def test_shared_agent_admin_allowed() -> None: row = SimpleNamespace(user_id=None) user = SimpleNamespace(id=1, is_admin=True) assert_agent_owner(row, user) + + +def test_shared_agent_accessible_to_non_owner() -> None: + row = SimpleNamespace(user_id=1, is_shared=1) + user = SimpleNamespace(id=2, is_admin=False) + assert_agent_access_row(row, user) + + +def test_private_agent_forbidden_to_non_owner() -> None: + row = SimpleNamespace(user_id=1, is_shared=0) + user = SimpleNamespace(id=2, is_admin=False) + with pytest.raises(OctopError) as ei: + assert_agent_access_row(row, user) + assert ei.value.code == ErrorCode.FORBIDDEN + + +def test_shared_non_owner_still_fails_owner_assert() -> None: + row = SimpleNamespace(user_id=1, is_shared=1) + user = SimpleNamespace(id=2, is_admin=False) + with pytest.raises(OctopError): + assert_agent_owner(row, user) + + +def test_agent_is_shared_true_when_flag_set() -> None: + row = SimpleNamespace(is_shared=1) + assert agent_is_shared(row) is True + + +def test_agent_is_shared_false_when_missing_or_zero() -> None: + assert agent_is_shared(SimpleNamespace(is_shared=0)) is False + assert agent_is_shared(SimpleNamespace()) is False + + +def test_user_owns_agent_only_for_matching_owner() -> None: + row = SimpleNamespace(user_id=1) + owner = SimpleNamespace(id=1, is_admin=False) + other = SimpleNamespace(id=2, is_admin=False) + admin = SimpleNamespace(id=99, is_admin=True) + assert user_owns_agent(row, owner) is True + assert user_owns_agent(row, other) is False + assert user_owns_agent(row, admin) is False diff --git a/tests/unit/api/test_chat_composer_context.py b/tests/unit/api/test_chat_composer_context.py index 5d713360..eb6e2c76 100644 --- a/tests/unit/api/test_chat_composer_context.py +++ b/tests/unit/api/test_chat_composer_context.py @@ -2,9 +2,44 @@ from __future__ import annotations +from types import SimpleNamespace + +import pytest + +from octop.api.routers.chat.turn import resolve_thread_id +from octop.infra.errors import ErrorCode, OctopError from octop.infra.gateway.process.message_keys import build_composer_context +class _ThreadRegistry: + def __init__(self, row: object) -> None: + self.row = row + self.rebound = False + + def get_thread(self, thread_id: str) -> object: + assert thread_id == "owner-thread" + return self.row + + async def rebind(self, **_kwargs: object) -> None: + self.rebound = True + + +async def test_resolve_thread_id_rejects_another_users_thread() -> None: + registry = _ThreadRegistry(SimpleNamespace(agent_id="shared-agent", user_id=1)) + + with pytest.raises(OctopError) as exc_info: + await resolve_thread_id( + agent_id="shared-agent", + user_id=2, + thread_registry=registry, + thread_id="owner-thread", + session_key=None, + ) + + assert exc_info.value.code == ErrorCode.FORBIDDEN + assert registry.rebound is False + + def test_build_composer_context_omits_default_model() -> None: ctx = build_composer_context( mcp_servers=["github"], diff --git a/tests/unit/api/test_chat_history.py b/tests/unit/api/test_chat_history.py index d6afe5cf..2020adbd 100644 --- a/tests/unit/api/test_chat_history.py +++ b/tests/unit/api/test_chat_history.py @@ -154,7 +154,7 @@ async def test_list_threads_derives_has_messages_from_db() -> None: @pytest.mark.asyncio async def test_get_thread_history_returns_has_more(monkeypatch: pytest.MonkeyPatch) -> None: server = MagicMock() - row = MagicMock(agent_id="agt_1") + row = MagicMock(agent_id="agt_1", user_id=1) server.app_runtime.gateway.thread_registry.get_thread.return_value = row monkeypatch.setattr( diff --git a/tests/unit/api/test_jwt_auth_middleware.py b/tests/unit/api/test_jwt_auth_middleware.py index 01e9a804..9262001b 100644 --- a/tests/unit/api/test_jwt_auth_middleware.py +++ b/tests/unit/api/test_jwt_auth_middleware.py @@ -25,8 +25,14 @@ def test_exempt_paths() -> None: assert is_jwt_exempt_path("/api/health/") assert is_jwt_exempt_path("/api/setup/status") assert is_jwt_exempt_path("/api/auth/login") + assert is_jwt_exempt_path("/api/auth/oidc/status") + assert is_jwt_exempt_path("/api/auth/oidc/start") + assert is_jwt_exempt_path("/api/auth/oidc/callback") + assert is_jwt_exempt_path("/api/auth/oidc/exchange") assert is_jwt_exempt_path("/api/docs") assert is_jwt_exempt_path("/api/openapi.json") + assert not is_jwt_exempt_path("/api/auth/oidc/config") + assert not is_jwt_exempt_path("/api/auth/oidc/config/test") assert not is_jwt_exempt_path("/api/auth/me") assert not is_jwt_exempt_path("/api/agents") diff --git a/tests/unit/api/test_knowledge_bases.py b/tests/unit/api/test_knowledge_bases.py new file mode 100644 index 00000000..4f656e65 --- /dev/null +++ b/tests/unit/api/test_knowledge_bases.py @@ -0,0 +1,447 @@ +"""Unit tests for the knowledge-base HTTP adapter.""" + +from __future__ import annotations + +from dataclasses import dataclass +from types import SimpleNamespace + +import pytest +from starlette.datastructures import Headers, UploadFile +from starlette.requests import Request + +from octop.infra.errors import ErrorCode, OctopError + + +def _request() -> Request: + return Request( + {"type": "http", "method": "POST", "path": "/api/knowledge-bases", "headers": []} + ) + + +def _services(**extra: object) -> SimpleNamespace: + return SimpleNamespace( + settings_repo=SimpleNamespace(get=lambda _key: None, set=lambda *_: None), + provider_repo=SimpleNamespace(list_all=lambda: []), + **extra, + ) + + +@dataclass +class _Base: + id: str = "kb-1" + owner_user_id: int = 1 + name: str = "Docs" + description: str = "" + default_open: bool = False + shared: bool = False + icon_name: str = "" + embedding_model: str = "model" + embedding_dim: int = 0 + doc_count: int = 0 + created_at: int = 1 + updated_at: int = 1 + + +@dataclass +class _Document: + id: str = "doc-1" + kb_id: str = "kb-1" + filename: str = "readme.md" + content_type: str = "text/markdown" + byte_size: int = 2 + content_hash: str = "" + status: str = "pending" + error_message: str = "" + chunk_count: int = 0 + created_at: int = 1 + updated_at: int = 1 + + +@pytest.mark.asyncio +async def test_enabling_without_model_maps_prerequisite_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + server = SimpleNamespace(services=_services()) + + def fail(*_args: object, **_kwargs: object) -> None: + raise ValueError("enabling knowledge bases requires an embedding model") + + monkeypatch.setattr(knowledge_bases, "set_feature_enabled", fail) + + with pytest.raises(OctopError) as raised: + await knowledge_bases.put_feature( + knowledge_bases.FeatureBody(enabled=True), + request=_request(), + server=server, + _admin=object(), + ) + + assert raised.value.code == ErrorCode.KNOWLEDGE_PREREQUISITES_FAILED + + +@pytest.mark.asyncio +async def test_enabling_reindexes_documents_when_model_changes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + settings = { + "knowledge_bases_enabled": "true", + "knowledge_embedding_model": "old-model", + "knowledge_embedding_backend": "onnx", + "knowledge_embedding_provider_id": "", + } + calls: list[tuple[object, str]] = [] + server = SimpleNamespace( + services=SimpleNamespace( + settings_repo=SimpleNamespace(get=settings.get, set=settings.__setitem__), + provider_repo=SimpleNamespace(list_all=lambda: []), + ) + ) + + def set_feature( + _get: object, + settings_set: object, + *, + enabled: bool, + model: str | None, + backend: str | None = None, + provider_id: str | None = None, + provider_repo: object = None, + ) -> None: + assert enabled is True + assert model == "new-model" + settings_set("knowledge_embedding_model", model) + + monkeypatch.setattr(knowledge_bases, "set_feature_enabled", set_feature) + monkeypatch.setattr( + knowledge_bases, + "reindex_all_documents", + lambda services, model: calls.append((services, model)), + ) + monkeypatch.setattr( + knowledge_bases, + "get_capability", + lambda get, _provider_repo=None: { + "feature_enabled": get("knowledge_bases_enabled") == "true", + "selected_model": get("knowledge_embedding_model"), + "backend": get("knowledge_embedding_backend") or "onnx", + "provider_id": get("knowledge_embedding_provider_id") or "", + }, + ) + + await knowledge_bases.put_feature( + knowledge_bases.FeatureBody(enabled=True, model="new-model"), + request=_request(), + server=server, + _admin=object(), + ) + + assert calls == [(server.services, "new-model")] + + +@pytest.mark.asyncio +async def test_create_base_maps_disabled_feature_error(monkeypatch: pytest.MonkeyPatch) -> None: + from octop.api.routers import knowledge_bases + + server = SimpleNamespace(services=_services()) + user = SimpleNamespace(id=1, is_admin=False) + + def fail(*_args: object, **_kwargs: object) -> None: + raise RuntimeError("knowledge feature is disabled") + + monkeypatch.setattr(knowledge_bases, "assert_knowledge_usable", fail) + + with pytest.raises(OctopError) as raised: + await knowledge_bases.create_base( + knowledge_bases.CreateBaseBody(name="Docs"), + request=_request(), + server=server, + user=user, + ) + + assert raised.value.code == ErrorCode.KNOWLEDGE_FEATURE_DISABLED + + +@pytest.mark.asyncio +async def test_create_base_uses_selected_model_when_usable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + created: dict[str, object] = {} + service = SimpleNamespace( + create_base=lambda **kwargs: created.update(kwargs) or _Base(), + ) + server = SimpleNamespace(services=_services()) + user = SimpleNamespace(id=1, is_admin=False) + monkeypatch.setattr(knowledge_bases, "assert_knowledge_usable", lambda *_a, **_k: None) + monkeypatch.setattr(knowledge_bases, "_knowledge_service", lambda _server: service) + + response = await knowledge_bases.create_base( + knowledge_bases.CreateBaseBody(name="Docs"), + request=_request(), + server=server, + user=user, + ) + + assert response["id"] == "kb-1" + assert created == { + "owner_user_id": 1, + "name": "Docs", + "description": "", + "default_open": False, + "shared": False, + "icon_name": "", + } + + +@pytest.mark.asyncio +async def test_upload_enqueues_document_indexing(monkeypatch: pytest.MonkeyPatch) -> None: + from octop.api.routers import knowledge_bases + + calls: list[tuple[object, str, str]] = [] + service = SimpleNamespace( + upload_document=lambda *_args, **_kwargs: _Document(), + ) + server = SimpleNamespace(services=_services()) + user = SimpleNamespace(id=1, is_admin=False) + upload = UploadFile( + file=__import__("io").BytesIO(b"# hi"), + filename="readme.md", + headers=Headers({"content-type": "text/markdown"}), + ) + monkeypatch.setattr(knowledge_bases, "_knowledge_service", lambda _server: service) + monkeypatch.setattr(knowledge_bases, "assert_knowledge_usable", lambda *_a, **_k: None) + monkeypatch.setattr( + knowledge_bases, + "enqueue_index_document", + lambda services, kb_id, doc_id: calls.append((services, kb_id, doc_id)), + ) + + response = await knowledge_bases.upload_document( + "kb-1", request=_request(), upload=upload, server=server, user=user + ) + + assert response["id"] == "doc-1" + assert calls == [(server.services, "kb-1", "doc-1")] + + +def test_base_payload_includes_owner_display_fields() -> None: + from octop.api.routers import knowledge_bases + + owner = SimpleNamespace(username="Test", display_name=None) + server = SimpleNamespace( + services=SimpleNamespace(user_repo=SimpleNamespace(get=lambda _id: owner)) + ) + base = _Base(owner_user_id=7) + + payload = knowledge_bases._base_payload(server, base) + + assert payload["owner_user_id"] == 7 + assert payload["owner_username"] == "Test" + assert payload["owner_display_name"] == "Test" + + +@pytest.mark.asyncio +async def test_preview_document_returns_extracted_text( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + service = SimpleNamespace( + preview_document=lambda *_args, **_kwargs: { + "id": "doc-1", + "filename": "notes.md", + "text": "# Hello", + } + ) + server = SimpleNamespace(services=_services()) + user = SimpleNamespace(id=1, is_admin=False) + monkeypatch.setattr(knowledge_bases, "_knowledge_service", lambda _server: service) + + response = await knowledge_bases.preview_document( + "kb-1", "doc-1", request=_request(), server=server, user=user + ) + + assert response == {"id": "doc-1", "filename": "notes.md", "text": "# Hello"} + + +@pytest.mark.asyncio +async def test_embedding_options_excludes_onnx_local_provider() -> None: + from octop.api.routers import knowledge_bases + + providers = [ + SimpleNamespace( + id=1, + name="ONNX (Local)", + api_key="onnx", + get_models=lambda: [ + { + "id": "BAAI/bge-small-zh-v1.5", + "name": "bge", + "embedding": True, + } + ], + ), + SimpleNamespace( + id=2, + name="OpenAI", + api_key="sk-test", + get_models=lambda: [ + {"id": "text-embedding-3-small", "name": "embed", "embedding": True}, + {"id": "gpt-4o", "name": "GPT-4o"}, + ], + ), + ] + server = SimpleNamespace( + services=SimpleNamespace(provider_repo=SimpleNamespace(list_all=lambda: providers)) + ) + + options = await knowledge_bases.embedding_options(server=server, _admin=object()) + + assert [row["provider_name"] for row in options["remote"]] == ["OpenAI"] + assert options["remote"][0]["models"] == [{"id": "text-embedding-3-small", "name": "embed"}] + assert isinstance(options["onnx"], list) + from octop.infra.agents.providers.onnx_catalog import ONNX_PRESET_MODEL_IDS + + onnx_ids = [row["id"] for row in options["onnx"]] + assert onnx_ids == list(ONNX_PRESET_MODEL_IDS) + assert all(row.get("recommended") for row in options["onnx"]) + assert all("downloaded" in row for row in options["onnx"]) + assert any(row.get("size_gb") for row in options["onnx"]) + + +@pytest.mark.asyncio +async def test_embedding_options_omits_non_recommended_onnx() -> None: + from octop.api.routers import knowledge_bases + from octop.infra.agents.providers.onnx_catalog import ONNX_PRESET_MODEL_IDS + + extra = "BAAI/bge-small-en-v1.5" + server = SimpleNamespace( + services=SimpleNamespace( + provider_repo=SimpleNamespace(list_all=lambda: []), + settings_repo=SimpleNamespace( + get=lambda key: extra if key == "knowledge_embedding_model" else None + ), + ) + ) + + options = await knowledge_bases.embedding_options(server=server, _admin=object()) + + assert extra not in [row["id"] for row in options["onnx"]] + assert [row["id"] for row in options["onnx"]] == list(ONNX_PRESET_MODEL_IDS) + + +@pytest.mark.asyncio +async def test_embedding_options_all_onnx_includes_extras() -> None: + from octop.api.routers import knowledge_bases + from octop.infra.agents.providers.onnx_catalog import ONNX_PRESET_MODEL_IDS + + server = SimpleNamespace( + services=SimpleNamespace(provider_repo=SimpleNamespace(list_all=lambda: [])) + ) + + options = await knowledge_bases.embedding_options(all_onnx=True, server=server, _admin=object()) + + ids = [row["id"] for row in options["onnx"]] + assert ids[: len(ONNX_PRESET_MODEL_IDS)] == list(ONNX_PRESET_MODEL_IDS) + assert len(ids) > len(ONNX_PRESET_MODEL_IDS) + assert any(not row.get("recommended") for row in options["onnx"]) + assert all("downloaded" in row for row in options["onnx"]) + + +@pytest.mark.asyncio +async def test_reindex_document_enqueues_job( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + calls: list[tuple[object, str, str]] = [] + service = SimpleNamespace( + reindex_document=lambda *_args, **_kwargs: _Document(status="pending"), + ) + server = SimpleNamespace(services=_services()) + user = SimpleNamespace(id=1, is_admin=False) + monkeypatch.setattr(knowledge_bases, "_knowledge_service", lambda _server: service) + monkeypatch.setattr(knowledge_bases, "_require_usable", lambda *_a, **_k: None) + monkeypatch.setattr( + knowledge_bases, + "enqueue_index_document", + lambda services, kb_id, doc_id: calls.append((services, kb_id, doc_id)), + ) + + response = await knowledge_bases.reindex_document( + "kb-1", "doc-1", request=_request(), server=server, user=user + ) + + assert response["status"] == "pending" + assert calls == [(server.services, "kb-1", "doc-1")] + + +@pytest.mark.asyncio +async def test_onnx_download_starts_catalog_model(monkeypatch: pytest.MonkeyPatch) -> None: + from octop.api.routers import knowledge_bases + + class _State: + def to_dict(self) -> dict[str, object]: + return { + "status": "downloading", + "progress": 0.1, + "model_name": "BAAI/bge-small-zh-v1.5", + "error": None, + } + + async def start(model: str) -> _State: + assert model == "BAAI/bge-small-zh-v1.5" + return _State() + + monkeypatch.setattr(knowledge_bases.DOWNLOAD_MANAGER, "start_download", start) + + response = await knowledge_bases.start_onnx_download( + knowledge_bases.OnnxDownloadBody(model="BAAI/bge-small-zh-v1.5"), + request=_request(), + _=object(), + ) + + assert response["status"] == "downloading" + assert response["model_name"] == "BAAI/bge-small-zh-v1.5" + + +@pytest.mark.asyncio +async def test_onnx_activate_enables_downloaded_service( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.api.routers import knowledge_bases + + saved: dict[str, object] = {} + + async def ready(**_kwargs: object) -> str: + return "ready" + + monkeypatch.setattr(knowledge_bases, "ensure_local_embedding_deps_async", ready) + monkeypatch.setattr(knowledge_bases, "assert_catalog_model", lambda model: model) + monkeypatch.setattr(knowledge_bases, "is_model_downloaded", lambda _model: True) + monkeypatch.setattr( + knowledge_bases, + "save_config", + lambda _setter, config: saved.update(config.to_dict()) or config, + ) + monkeypatch.setattr( + knowledge_bases, + "status_payload", + lambda *_args, **_kwargs: {"enabled": True, "model": "BAAI/bge-small-zh-v1.5"}, + ) + server = SimpleNamespace(services=_services()) + + response = await knowledge_bases.activate_onnx_service( + knowledge_bases.OnnxDownloadBody(model="BAAI/bge-small-zh-v1.5"), + request=_request(), + server=server, + _=object(), + ) + + assert saved == {"enabled": True, "model": "BAAI/bge-small-zh-v1.5"} + assert response["enabled"] is True diff --git a/tests/unit/api/test_ollama_service_default.py b/tests/unit/api/test_ollama_service_default.py new file mode 100644 index 00000000..de913972 --- /dev/null +++ b/tests/unit/api/test_ollama_service_default.py @@ -0,0 +1,20 @@ +"""Unit tests for Ollama local service enablement defaults.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +from octop.api.routers import ollama_models + + +def test_ollama_service_defaults_to_disabled_when_unset() -> None: + server = SimpleNamespace(services=SimpleNamespace(settings_repo=MagicMock())) + server.services.settings_repo.get.return_value = None + assert ollama_models._ollama_service_enabled(server) is False + + +def test_ollama_service_respects_stored_true() -> None: + server = SimpleNamespace(services=SimpleNamespace(settings_repo=MagicMock())) + server.services.settings_repo.get.return_value = "true" + assert ollama_models._ollama_service_enabled(server) is True diff --git a/tests/unit/api/test_permissions_api.py b/tests/unit/api/test_permissions_api.py new file mode 100644 index 00000000..cd8573b1 --- /dev/null +++ b/tests/unit/api/test_permissions_api.py @@ -0,0 +1,38 @@ +"""Unit tests for require_permission dependency.""" + +from __future__ import annotations + +import pytest + +from octop.api.deps import require_permission +from octop.infra.errors import ErrorCode, OctopError +from octop.infra.users.identity import Role, User + + +def _user(is_admin: bool, permissions: list[str] | None = None) -> User: + return User( + id=1, + username="u", + role=Role.ADMIN if is_admin else Role.USER, + display_name=None, + permissions=permissions or [], + ) + + +def test_unknown_key_raises_at_construction() -> None: + with pytest.raises(RuntimeError): + require_permission("not_a_real_key") + + +async def test_dependency_denies_without_permission() -> None: + dep = require_permission("browser") + with pytest.raises(OctopError) as exc: + await dep(_user(False, [])) + assert exc.value.code is ErrorCode.FORBIDDEN + assert exc.value.details.get("permission") == "browser" + + +async def test_dependency_allows_admin_and_holder() -> None: + dep = require_permission("browser") + assert await dep(_user(True)) is not None + assert await dep(_user(False, ["browser"])) is not None diff --git a/tests/unit/api/test_terminal_sessions.py b/tests/unit/api/test_terminal_sessions.py index 2de35570..1da72604 100644 --- a/tests/unit/api/test_terminal_sessions.py +++ b/tests/unit/api/test_terminal_sessions.py @@ -200,7 +200,7 @@ async def close(self, code: int = 1000, reason: str | None = None) -> None: def _make_server(*, has_agent: bool = True) -> tuple[object, Mock]: - get_row = Mock(return_value=SimpleNamespace(name="bot") if has_agent else None) + get_row = Mock(return_value=SimpleNamespace(name="bot", user_id=1) if has_agent else None) server = SimpleNamespace( app_runtime=SimpleNamespace(agent_registry=SimpleNamespace(get_row=get_row)), services=SimpleNamespace( @@ -216,7 +216,9 @@ def _sent_messages(ws: _FakeWS) -> list[dict[str, object]]: def _patch_user(monkeypatch, user_id: int = 1) -> None: monkeypatch.setattr( - terminal, "resolve_user_from_token", lambda _s, _t: SimpleNamespace(id=user_id) + terminal, + "resolve_user_from_token", + lambda _s, _t: SimpleNamespace(id=user_id, is_admin=False, permissions=["terminal"]), ) diff --git a/tests/unit/api/test_update_router.py b/tests/unit/api/test_update_router.py index 4304ae93..bcb6f54a 100644 --- a/tests/unit/api/test_update_router.py +++ b/tests/unit/api/test_update_router.py @@ -5,11 +5,57 @@ import pytest from octop.api.routers import update as update_router +from octop.api.routers import update_store from octop.api.routers.update_store import UpgradeTaskStatus, create_task, get_task from octop.infra.errors import ErrorCode, OctopError from octop.infra.setup.self_update import UpgradeResult +@pytest.fixture(autouse=True) +def _clear_update_status_cache() -> None: + update_store.clear_cached_status() + yield + update_store.clear_cached_status() + + +@pytest.mark.asyncio +async def test_update_status_reprobes_after_cache_ttl( + monkeypatch: pytest.MonkeyPatch, +) -> None: + builds = 0 + + def fake_build(**_: object) -> dict[str, object]: + nonlocal builds + builds += 1 + payload = { + "current_version": "1.0.0", + "latest_version": "1.0.1", + "has_update": True, + "is_editable": False, + "service_mode": None, + "error": None, + "last_check_time": "2026-01-01T00:00:00Z", + "release_notes": None, + } + update_store.cache_status(payload) + return payload + + monkeypatch.setattr(update_router, "_build_status", fake_build) + + first = await update_router.update_status(_=None) + second = await update_router.update_status(_=None) + assert first == second + assert builds == 1 + + update_store.cache_status( + first, + cached_at=0.0, # force expiry on next read + ) + third = await update_router.update_status(_=None) + assert third["has_update"] is True + assert builds == 2 + + @pytest.mark.asyncio async def test_restart_endpoint_schedules_background_restart( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/unit/api/test_update_store.py b/tests/unit/api/test_update_store.py new file mode 100644 index 00000000..583e0b74 --- /dev/null +++ b/tests/unit/api/test_update_store.py @@ -0,0 +1,30 @@ +"""Unit tests for /api/update in-memory status cache.""" + +from __future__ import annotations + +from octop.api.routers import update_store + + +def setup_function() -> None: + update_store.clear_cached_status() + + +def teardown_function() -> None: + update_store.clear_cached_status() + + +def test_get_cached_status_miss_when_empty() -> None: + assert update_store.get_cached_status() is None + + +def test_get_cached_status_hit_within_ttl() -> None: + payload = {"current_version": "1.0.0", "has_update": False} + update_store.cache_status(payload, cached_at=1_000.0) + assert update_store.get_cached_status(now=1_000.0 + 60) == payload + + +def test_get_cached_status_expires_after_ttl() -> None: + payload = {"current_version": "1.0.0", "has_update": False} + update_store.cache_status(payload, cached_at=1_000.0) + expired_at = 1_000.0 + update_store.STATUS_CACHE_TTL_SECONDS + 1 + assert update_store.get_cached_status(now=expired_at) is None diff --git a/tests/unit/auth/test_public_base.py b/tests/unit/auth/test_public_base.py new file mode 100644 index 00000000..7562737f --- /dev/null +++ b/tests/unit/auth/test_public_base.py @@ -0,0 +1,45 @@ +"""Tests for public OIDC callback URL helpers.""" + +from __future__ import annotations + +from starlette.requests import Request + +from octop.api.common.public_base import resolve_public_base +from octop.infra.auth.sso.public_base import build_redirect_uri + + +def _request(*, scheme: str = "http", headers: list[tuple[bytes, bytes]] | None = None) -> Request: + return Request( + { + "type": "http", + "scheme": scheme, + "server": ("internal.example", 8080), + "path": "/", + "headers": headers or [(b"host", b"internal.example:8080")], + } + ) + + +def test_resolve_public_base_uses_forwarded_proxy_headers() -> None: + request = _request( + headers=[ + (b"host", b"internal.example:8080"), + (b"x-forwarded-proto", b"https"), + (b"x-forwarded-host", b"octop.example"), + ] + ) + + assert resolve_public_base(request) == "https://octop.example" + + +def test_resolve_public_base_uses_request_url_without_proxy_headers() -> None: + request = _request(scheme="https", headers=[(b"host", b"octop.example")]) + + assert resolve_public_base(request) == "https://octop.example" + + +def test_build_redirect_uri_uses_oidc_callback_path() -> None: + assert ( + build_redirect_uri("https://octop.example/") + == "https://octop.example/api/auth/oidc/callback" + ) diff --git a/tests/unit/auth/test_sso_crypto.py b/tests/unit/auth/test_sso_crypto.py new file mode 100644 index 00000000..fc217c15 --- /dev/null +++ b/tests/unit/auth/test_sso_crypto.py @@ -0,0 +1,22 @@ +"""Tests for SSO secret encryption.""" + +from __future__ import annotations + +from pathlib import Path + +from octop.infra.auth.sso.crypto import decrypt_secret, encrypt_secret +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.secrets import SecretRepo + + +def test_sso_secret_round_trip_uses_its_own_fernet_key(tmp_path: Path) -> None: + db = SqlitePool(tmp_path / "octop.db") + run_migrations(db) + secret_repo = SecretRepo(db) + + encrypted = encrypt_secret(secret_repo, "sensitive value") + + assert decrypt_secret(secret_repo, encrypted) == "sensitive value" + assert secret_repo.get("sso_fernet") is not None + assert secret_repo.get("connector_fernet") is None diff --git a/tests/unit/auth/test_sso_discovery.py b/tests/unit/auth/test_sso_discovery.py new file mode 100644 index 00000000..a18843ad --- /dev/null +++ b/tests/unit/auth/test_sso_discovery.py @@ -0,0 +1,50 @@ +"""Tests for OIDC discovery helpers.""" + +from __future__ import annotations + +import httpx +import pytest + +from octop.infra.auth.sso.discovery import DiscoveryCache, fetch_discovery + + +def test_fetch_discovery_requests_openid_configuration() -> None: + requested: list[str] = [] + + def handler(request: httpx.Request) -> httpx.Response: + requested.append(str(request.url)) + return httpx.Response(200, json={"issuer": "https://issuer.example"}) + + with httpx.Client(transport=httpx.MockTransport(handler)) as client: + assert fetch_discovery("https://issuer.example/", httpx_client=client) == { + "issuer": "https://issuer.example" + } + + assert requested == ["https://issuer.example/.well-known/openid-configuration"] + + +def test_fetch_discovery_rejects_issuer_mismatch() -> None: + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json={"issuer": "https://other.example"}) + + with ( + httpx.Client(transport=httpx.MockTransport(handler)) as client, + pytest.raises(ValueError, match="does not match"), + ): + fetch_discovery("https://issuer.example", httpx_client=client) + + +def test_discovery_cache_reuses_value_until_invalidated() -> None: + calls = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(200, json={"issuer": "https://issuer.example", "calls": calls}) + + cache = DiscoveryCache() + with httpx.Client(transport=httpx.MockTransport(handler)) as client: + assert cache.get("https://issuer.example", httpx_client=client)["calls"] == 1 + assert cache.get("https://issuer.example", httpx_client=client)["calls"] == 1 + cache.invalidate("https://issuer.example") + assert cache.get("https://issuer.example", httpx_client=client)["calls"] == 2 diff --git a/tests/unit/auth/test_sso_id_token.py b/tests/unit/auth/test_sso_id_token.py new file mode 100644 index 00000000..cb81b705 --- /dev/null +++ b/tests/unit/auth/test_sso_id_token.py @@ -0,0 +1,105 @@ +"""Tests for OIDC ID token verification.""" + +from __future__ import annotations + +import json +import time + +import httpx +import jwt +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa + +from octop.infra.auth.sso.id_token import verify_id_token + + +@pytest.fixture +def signing_key() -> rsa.RSAPrivateKey: + return rsa.generate_private_key(public_exponent=65537, key_size=2048) + + +@pytest.fixture +def jwks_client(signing_key: rsa.RSAPrivateKey) -> httpx.Client: + jwk = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(signing_key.public_key())) + jwk.update({"kid": "test-key", "use": "sig", "alg": "RS256"}) + + def handler(request: httpx.Request) -> httpx.Response: + assert str(request.url) == "https://issuer.example/jwks" + return httpx.Response(200, json={"keys": [jwk]}) + + return httpx.Client(transport=httpx.MockTransport(handler)) + + +def _token( + signing_key: rsa.RSAPrivateKey, *, issuer: str = "https://issuer.example", nonce: str = "nonce" +) -> str: + now = int(time.time()) + return jwt.encode( + { + "iss": issuer, + "aud": ["octop-client", "other-client"], + "exp": now + 300, + "iat": now, + "nonce": nonce, + "sub": "user-1", + }, + signing_key, + algorithm="RS256", + headers={"kid": "test-key"}, + ) + + +def test_verify_id_token_rejects_wrong_issuer( + signing_key: rsa.RSAPrivateKey, jwks_client: httpx.Client +) -> None: + with pytest.raises(jwt.InvalidIssuerError): + verify_id_token( + _token(signing_key, issuer="https://other-issuer.example"), + jwks_uri="https://issuer.example/jwks", + issuer="https://issuer.example", + client_id="octop-client", + nonce="nonce", + httpx=jwks_client, + ) + + +def test_verify_id_token_rejects_wrong_nonce( + signing_key: rsa.RSAPrivateKey, jwks_client: httpx.Client +) -> None: + with pytest.raises(jwt.InvalidTokenError, match="nonce"): + verify_id_token( + _token(signing_key, nonce="unexpected"), + jwks_uri="https://issuer.example/jwks", + issuer="https://issuer.example", + client_id="octop-client", + nonce="nonce", + httpx=jwks_client, + ) + + +def test_verify_id_token_rejects_disallowed_algorithm( + signing_key: rsa.RSAPrivateKey, jwks_client: httpx.Client +) -> None: + now = int(time.time()) + hs_token = jwt.encode( + { + "iss": "https://issuer.example", + "aud": "octop-client", + "exp": now + 300, + "iat": now, + "nonce": "nonce", + "sub": "user-1", + }, + "symmetric-secret-at-least-32-bytes!!", + algorithm="HS256", + headers={"kid": "test-key"}, + ) + with pytest.raises(jwt.InvalidTokenError, match="not allowed"): + verify_id_token( + hs_token, + jwks_uri="https://issuer.example/jwks", + issuer="https://issuer.example", + client_id="octop-client", + nonce="nonce", + httpx=jwks_client, + ) diff --git a/tests/unit/auth/test_sso_pkce.py b/tests/unit/auth/test_sso_pkce.py new file mode 100644 index 00000000..9aa567c8 --- /dev/null +++ b/tests/unit/auth/test_sso_pkce.py @@ -0,0 +1,21 @@ +"""Tests for OIDC PKCE helpers.""" + +from __future__ import annotations + +import base64 +import hashlib + +from octop.infra.auth.sso.pkce import new_pkce_pair + + +def test_new_pkce_pair_uses_sha256_urlsafe_challenge() -> None: + verifier, challenge = new_pkce_pair() + + expected = ( + base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("ascii")).digest()) + .rstrip(b"=") + .decode("ascii") + ) + + assert verifier + assert challenge == expected diff --git a/tests/unit/auth/test_sso_redirect_after.py b/tests/unit/auth/test_sso_redirect_after.py new file mode 100644 index 00000000..e6596310 --- /dev/null +++ b/tests/unit/auth/test_sso_redirect_after.py @@ -0,0 +1,27 @@ +"""Tests for post-login redirect sanitization.""" + +from __future__ import annotations + +import pytest + +from octop.infra.auth.sso.redirect_after import sanitize_redirect_after + + +@pytest.mark.parametrize( + ("path", "expected"), + [ + (None, "/chat"), + ("", "/chat"), + ("/chat/thread-1", "/chat/thread-1"), + ("/settings", "/settings"), + ("https://attacker.example", "/chat"), + ("//attacker.example", "/chat"), + ("/\\attacker.example", "/chat"), + ("/chat?next=https://attacker.example", "/chat"), + ("chat", "/chat"), + ], +) +def test_sanitize_redirect_after_keeps_only_safe_internal_paths( + path: str | None, expected: str +) -> None: + assert sanitize_redirect_after(path) == expected diff --git a/tests/unit/auth/test_sso_service.py b/tests/unit/auth/test_sso_service.py new file mode 100644 index 00000000..76e781dd --- /dev/null +++ b/tests/unit/auth/test_sso_service.py @@ -0,0 +1,310 @@ +"""Tests for OIDC SSO service orchestration.""" + +from __future__ import annotations + +import time +from unittest.mock import patch + +import httpx +import jwt +import pytest + +from octop.config import OctopConfig +from octop.infra.auth.sso.crypto import decrypt_secret +from octop.infra.auth.sso.service import SsoService +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.services import build_shared_services +from octop.infra.users.identity import Role, User +from octop.infra.users.manager import UserManager +from octop.infra.utils.paths import PathLayout + + +@pytest.fixture +def service(tmp_path): + paths = PathLayout(tmp_path / ".octop") + paths.ensure_root() + db = SqlitePool(paths.db) + run_migrations(db) + services = build_shared_services(db=db, paths=paths, config=OctopConfig()) + return SsoService(services, UserManager(services)) + + +def configure_provider( + service: SsoService, + *, + enabled: bool = True, + issuer: str = "https://issuer.example/", + client_id: str = "client-id", + display_name: str = "Example ID", + dashboard_origin: str | None = None, +): + return service._services.sso_repo.upsert_provider( + enabled=enabled, + display_name=display_name, + issuer=issuer, + client_id=client_id, + client_secret_enc=None, + scopes="openid profile email", + dashboard_origin=dashboard_origin, + ) + + +def add_login_state(service: SsoService, provider_id: int, state: str = "valid-state") -> None: + service._services.sso_repo.create_login_state( + state=state, + provider_id=provider_id, + nonce="expected-nonce", + code_verifier="pkce-verifier", + redirect_after="/chat", + expires_at=int(time.time()) + 600, + ) + + +def discovery_client(handler): + real_httpx_client = httpx.Client + + def factory(*args: object, **kwargs: object) -> httpx.Client: + kwargs.pop("transport", None) + return real_httpx_client( + *args, + transport=httpx.MockTransport(handler), + **kwargs, # type: ignore[arg-type] + ) + + return patch("octop.infra.auth.sso.service.httpx.Client", side_effect=factory) + + +@pytest.mark.parametrize( + ("provider", "user_count"), + [ + (None, 0), + ({"enabled": False}, 1), + ({"issuer": " "}, 1), + ({}, 0), + ], +) +def test_status_is_disabled_without_usable_provider_or_users( + service: SsoService, provider: dict[str, object] | None, user_count: int +) -> None: + if provider is not None: + configure_provider( + service, + enabled=bool(provider.get("enabled", True)), + issuer=str(provider.get("issuer", "https://issuer.example/")), + ) + with patch.object(service._user_manager, "count", return_value=user_count): + assert service.status()["enabled"] is False + + +def test_status_returns_enabled_provider_display_name(service: SsoService) -> None: + configure_provider(service, display_name="Company Login") + with patch.object(service._user_manager, "count", return_value=1): + assert service.status() == {"enabled": True, "display_name": "Company Login"} + + +def test_put_config_encrypts_with_supplied_secret_repo_and_redacts_for_admin( + service: SsoService, +) -> None: + with patch( + "octop.infra.auth.sso.service.encrypt_secret", return_value=b"ciphertext" + ) as encrypt: + result = service.put_config( + { + "enabled": True, + "display_name": "Company Login", + "issuer": "https://issuer.example/", + "client_id": "client-id", + "client_secret": "top-secret", + }, + secret_repo=service._services.secret_repo, + ) + + assert encrypt.call_args.args == (service._services.secret_repo, "top-secret") + assert result["has_client_secret"] is True + assert service.get_config_for_admin(public_base="https://octop.example") == { + "enabled": True, + "display_name": "Company Login", + "issuer": "https://issuer.example/", + "client_id": "client-id", + "scopes": "openid profile email", + "dashboard_origin": None, + "has_client_secret": True, + "redirect_uri": "https://octop.example/api/auth/oidc/callback", + } + assert "client_secret" not in result + assert service._services.sso_repo.get_provider().client_secret_enc == b"ciphertext" + + +def test_put_config_encrypts_secret_and_empty_secret_keeps_previous_value( + service: SsoService, +) -> None: + service.put_config( + { + "issuer": "https://issuer.example/", + "client_id": "client-id", + "client_secret": "top-secret", + } + ) + first = service._services.sso_repo.get_provider() + assert first is not None + assert decrypt_secret(service._services.secret_repo, first.client_secret_enc) == "top-secret" + + service.put_config({"client_secret": ""}) + second = service._services.sso_repo.get_provider() + assert second is not None + assert second.client_secret_enc == first.client_secret_enc + + +def test_start_login_when_disabled_raises(service: SsoService) -> None: + with pytest.raises(ValueError, match="disabled"): + service.start_login(redirect_after="/chat", public_base="https://octop.example") + + +def test_start_login_persists_pkce_state_and_uses_s256(service: SsoService) -> None: + service._services.user_repo.create(username="admin", role="admin") + provider = configure_provider(service) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.url.path == "/.well-known/openid-configuration" + return httpx.Response( + 200, + json={ + "issuer": "https://issuer.example", + "authorization_endpoint": "https://issuer.example/authorize", + }, + ) + + with discovery_client(handler): + started = service.start_login( + redirect_after="https://attacker.example", public_base="https://octop.example" + ) + + query = httpx.QueryParams(started["authorization_url"].split("?", 1)[1]) + assert started["state"] == query["state"] + persisted = service._services.sso_repo.take_login_state(query["state"]) + assert persisted is not None + assert persisted.provider_id == provider.id + assert persisted.nonce == query["nonce"] + assert persisted.code_verifier + assert query["code_challenge"] + assert query["code_challenge_method"] == "S256" + assert query["redirect_uri"] == "https://octop.example/api/auth/oidc/callback" + assert persisted.redirect_after == "/chat" + assert service._services.sso_repo.take_login_state(query["state"]) is None + + +@pytest.mark.asyncio +async def test_callback_error_redirect_prefers_dashboard_origin(service: SsoService) -> None: + configure_provider(service, dashboard_origin="https://dashboard.example") + denied = await service.handle_callback( + code=None, state=None, error="access_denied", public_base="https://api.example" + ) + assert denied.url == "https://dashboard.example/login?oidc_error=denied" + + +@pytest.mark.asyncio +async def test_callback_maps_denied_bad_state_and_invalid_token(service: SsoService) -> None: + denied = await service.handle_callback( + code=None, state=None, error="access_denied", public_base="https://octop.example" + ) + bad_state = await service.handle_callback( + code="code", state="unknown", error=None, public_base="https://octop.example" + ) + assert denied.url == "https://octop.example/login?oidc_error=denied" + assert bad_state.url == "https://octop.example/login?oidc_error=state" + + provider = configure_provider(service) + add_login_state(service, provider.id) + with patch.object(service, "_exchange_claims", side_effect=jwt.InvalidTokenError): + invalid = await service.handle_callback( + code="code", state="valid-state", error=None, public_base="https://octop.example" + ) + assert invalid.url == "https://octop.example/login?oidc_error=invalid_token" + + +@pytest.mark.asyncio +async def test_callback_exchanges_code_and_attaches_login_code(service: SsoService) -> None: + service._services.user_repo.create(username="admin", role="admin") + configure_provider(service) + requests: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + if request.url.path == "/.well-known/openid-configuration": + return httpx.Response( + 200, + json={ + "issuer": "https://issuer.example", + "authorization_endpoint": "https://issuer.example/authorize", + "token_endpoint": "https://issuer.example/token", + "jwks_uri": "https://issuer.example/jwks", + }, + ) + assert request.url.path == "/token" + return httpx.Response(200, json={"id_token": "signed-id-token"}) + + with ( + discovery_client(handler), + patch( + "octop.infra.auth.sso.service.verify_id_token", + return_value={ + "sub": "subject-123", + "email": "member@example.com", + "preferred_username": "member", + }, + ), + ): + started = service.start_login( + redirect_after="/settings", public_base="https://octop.example" + ) + state = httpx.QueryParams(started["authorization_url"].split("?", 1)[1])["state"] + result = await service.handle_callback( + code="authorization-code", + state=state, + error=None, + public_base="https://octop.example", + ) + + assert result.url.startswith("https://octop.example/login/oidc/complete#code=") + assert "redirect=%2Fsettings" in result.url + assert requests[-1].url == httpx.URL("https://issuer.example/token") + assert requests[-1].method == "POST" + user_id = service._services.sso_repo.consume_login_code( + httpx.QueryParams(result.url.split("#", 1)[1])["code"] + ) + assert user_id is not None + assert service._services.user_repo.get(user_id["user_id"]).username == "member" + + +@pytest.mark.asyncio +async def test_exchange_login_code_is_one_shot_and_writes_audit(service: SsoService) -> None: + user = User(id=42, username="member", role=Role.USER, display_name=None) + provider = configure_provider(service) + add_login_state(service, provider.id) + service._services.sso_repo.attach_login_code( + "valid-state", login_code="one-shot", user_id=user.id, expires_at=int(time.time()) + 60 + ) + with patch.object(service._user_manager, "get_by_id", return_value=user): + assert await service.exchange_login_code("one-shot") == user + with pytest.raises(ValueError, match="invalid or expired"): + await service.exchange_login_code("one-shot") + with pytest.raises(ValueError, match="invalid or expired"): + await service.exchange_login_code("unknown") + + audit = service._services.audit_repo.query(action="auth.oidc_login") + assert [(row.actor, row.target) for row in audit] == [("member", "member")] + + +@pytest.mark.asyncio +async def test_exchange_login_code_rejects_unavailable_user(service: SsoService) -> None: + provider = configure_provider(service) + add_login_state(service, provider.id) + service._services.sso_repo.attach_login_code( + "valid-state", login_code="unavailable", user_id=42, expires_at=int(time.time()) + 60 + ) + with ( + patch.object(service._user_manager, "get_by_id", return_value=None), + pytest.raises(ValueError, match="unavailable"), + ): + await service.exchange_login_code("unavailable") diff --git a/tests/unit/backup/test_auto.py b/tests/unit/backup/test_auto.py new file mode 100644 index 00000000..696368f4 --- /dev/null +++ b/tests/unit/backup/test_auto.py @@ -0,0 +1,88 @@ +"""Unit tests for automatic backup helpers.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from octop.config import BackupConfig, load_config +from octop.infra.backup.auto import ( + AUTO_BACKUP_JOB_ID, + apply_auto_backup_schedule, + backup_config_from_payload, + persist_backup_config, + to_auto_backup_filename, +) +from octop.infra.errors import OctopError + + +def test_to_auto_backup_filename() -> None: + assert ( + to_auto_backup_filename("octop-backup-20260101T000000Z.tar.gz") + == "octop-auto-backup-20260101T000000Z.tar.gz" + ) + assert ( + to_auto_backup_filename("octop-auto-backup-20260101T000000Z.tar.gz") + == "octop-auto-backup-20260101T000000Z.tar.gz" + ) + + +def test_backup_config_from_payload_rejects_bad_schedule() -> None: + with pytest.raises(OctopError): + backup_config_from_payload({"schedule": "not-a-trigger", "retention_count": 3}) + + +def test_backup_config_from_payload_rejects_bad_retention() -> None: + with pytest.raises(OctopError): + backup_config_from_payload({"schedule": "cron:0 4 * * *", "retention_count": 0}) + + +def test_persist_backup_config(tmp_path: Path) -> None: + cfg_path = tmp_path / "config.json" + load_config(cfg_path) + out = persist_backup_config( + cfg_path, + BackupConfig(auto_enabled=True, schedule="interval:3600", retention_count=3), + ) + assert out.backup.auto_enabled is True + assert out.backup.schedule == "interval:3600" + assert out.backup.retention_count == 3 + reloaded = load_config(cfg_path) + assert reloaded.backup.auto_enabled is True + + +def test_apply_auto_backup_schedule_skips_when_disabled(tmp_path: Path) -> None: + paths = MagicMock() + paths.config = tmp_path / "config.json" + load_config(paths.config) + + cron = MagicMock() + server = MagicMock() + server.paths = paths + + apply_auto_backup_schedule(cron, server=server) + cron.unschedule_system_job.assert_called_once_with(AUTO_BACKUP_JOB_ID) + cron.schedule_system_job.assert_not_called() + + +def test_apply_auto_backup_schedule_registers_when_enabled(tmp_path: Path) -> None: + paths = MagicMock() + paths.config = tmp_path / "config.json" + load_config(paths.config) + persist_backup_config( + paths.config, + BackupConfig(auto_enabled=True, schedule="cron:0 4 * * *", retention_count=7), + ) + + cron = MagicMock() + server = MagicMock() + server.paths = paths + + apply_auto_backup_schedule(cron, server=server) + cron.unschedule_system_job.assert_called_once_with(AUTO_BACKUP_JOB_ID) + cron.schedule_system_job.assert_called_once() + kwargs = cron.schedule_system_job.call_args + assert kwargs.args[0] == AUTO_BACKUP_JOB_ID + assert kwargs.kwargs["trigger"] == "cron:0 4 * * *" diff --git a/tests/unit/backup/test_store.py b/tests/unit/backup/test_store.py index 2a09285e..9cd6ba7d 100644 --- a/tests/unit/backup/test_store.py +++ b/tests/unit/backup/test_store.py @@ -9,8 +9,10 @@ from octop.infra.backup.store import ( delete_backup_file, + is_auto_backup_filename, list_backup_files, normalize_backup_filename, + prune_auto_backups, read_backup_file, write_backup_file, ) @@ -71,3 +73,41 @@ def test_created_at_falls_back_on_invalid_filename_stamp(tmp_path: Path) -> None items = list_backup_files(layout) assert len(items) == 1 datetime.fromisoformat(items[0].created_at) + + +def test_created_at_from_auto_filename(tmp_path: Path) -> None: + layout = PathLayout(tmp_path / ".octop") + name = "octop-auto-backup-20260721T020000Z.tar.gz" + write_backup_file(layout, name, b"x") + items = list_backup_files(layout) + assert len(items) == 1 + assert items[0].created_at == "2026-07-21T02:00:00+00:00" + assert is_auto_backup_filename(name) + assert not is_auto_backup_filename("octop-backup-20260721T020000Z.tar.gz") + + +def test_prune_auto_backups_keeps_newest_and_spares_manual(tmp_path: Path) -> None: + layout = PathLayout(tmp_path / ".octop") + write_backup_file(layout, "octop-backup-20260101T000000Z.tar.gz", b"manual") + write_backup_file(layout, "octop-auto-backup-20260101T010000Z.tar.gz", b"a1") + write_backup_file(layout, "octop-auto-backup-20260102T010000Z.tar.gz", b"a2") + write_backup_file(layout, "octop-auto-backup-20260103T010000Z.tar.gz", b"a3") + + deleted = prune_auto_backups(layout, keep=2) + assert set(deleted) == {"octop-auto-backup-20260101T010000Z.tar.gz"} + names = {f.name for f in list_backup_files(layout)} + assert names == { + "octop-backup-20260101T000000Z.tar.gz", + "octop-auto-backup-20260102T010000Z.tar.gz", + "octop-auto-backup-20260103T010000Z.tar.gz", + } + + +def test_prune_auto_backups_keep_zero_deletes_all_auto(tmp_path: Path) -> None: + layout = PathLayout(tmp_path / ".octop") + write_backup_file(layout, "octop-backup-20260101T000000Z.tar.gz", b"manual") + write_backup_file(layout, "octop-auto-backup-20260101T010000Z.tar.gz", b"a1") + deleted = prune_auto_backups(layout, keep=0) + assert deleted == ["octop-auto-backup-20260101T010000Z.tar.gz"] + names = {f.name for f in list_backup_files(layout)} + assert names == {"octop-backup-20260101T000000Z.tar.gz"} diff --git a/tests/unit/db/test_agent_is_shared.py b/tests/unit/db/test_agent_is_shared.py new file mode 100644 index 00000000..6e6fae5d --- /dev/null +++ b/tests/unit/db/test_agent_is_shared.py @@ -0,0 +1,31 @@ +# tests/unit/db/test_agent_is_shared.py +from __future__ import annotations + +from pathlib import Path + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.agents import AgentRepo +from octop.infra.db.repos.users import UserRepo + + +@pytest.fixture +def db(tmp_path: Path) -> SqlitePool: + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + return pool + + +def test_is_shared_default_zero_and_list_shared(db): + run_migrations(db) # or use fixture that already migrated + UserRepo(db).create(username="u1", password_hash="h", role="user") + UserRepo(db).create(username="u2", password_hash="h", role="user") + repo = AgentRepo(db) + repo.create(agent_id="a1", user_id=1, name="mine") + repo.create(agent_id="a2", user_id=2, name="theirs") + repo.set_shared("a2", True) + rows = repo.list_shared(exclude_user_id=1) + assert [r.agent_id for r in rows] == ["a2"] + assert rows[0].is_shared == 1 diff --git a/tests/unit/db/test_clip_thread_title.py b/tests/unit/db/test_clip_thread_title.py index 1bd69385..657bb1a7 100644 --- a/tests/unit/db/test_clip_thread_title.py +++ b/tests/unit/db/test_clip_thread_title.py @@ -82,7 +82,7 @@ def test_migration_003_repairs_stored_hard_cuts(tmp_path: Path) -> None: with pool.connect() as conn: v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] title = conn.execute("SELECT title FROM threads WHERE thread_id = ?", ("t1",)).fetchone()[0] - assert v == 4 + assert v == 6 assert title == "x" * 39 + "…" # Idempotent repair assert repair_all_legacy_thread_titles(pool) == 0 diff --git a/tests/unit/db/test_db_pool.py b/tests/unit/db/test_db_pool.py index 96f2e0e2..76353bce 100644 --- a/tests/unit/db/test_db_pool.py +++ b/tests/unit/db/test_db_pool.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import sqlite3 from pathlib import Path import pytest @@ -58,6 +59,12 @@ def test_run_migrations_creates_tables(db: SqlitePool): "proactive_care_config", "care_push_records", "skill_packages", + "published_experts", + "knowledge_bases", + "knowledge_base_members", + "knowledge_documents", + "sso_providers", + "sso_login_states", } assert expected.issubset(names) @@ -69,10 +76,12 @@ def test_run_migrations_idempotent(db: SqlitePool): cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)").fetchall()} cron_cols = {r["name"] for r in conn.execute("PRAGMA table_info(cron_jobs)").fetchall()} thread_cols = {r["name"] for r in conn.execute("PRAGMA table_info(threads)").fetchall()} - assert v == 4 + assert v == 6 assert "login_failed_count" in cols assert "login_locked_until" in cols assert "preferences_json" in cols + assert "permissions" in cols + assert {"email", "sso_provider_id", "sso_subject"}.issubset(cols) assert "task_type" in cron_cols assert "mcp_servers" in cron_cols assert {"model_ref", "reasoning_mode", "reasoning_effort"}.issubset(thread_cols) @@ -117,7 +126,7 @@ def test_migration_002_idempotent_when_column_already_present(tmp_path: Path) -> with pool.connect() as conn: v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] cron_cols = {r["name"] for r in conn.execute("PRAGMA table_info(cron_jobs)").fetchall()} - assert v == 4 + assert v == 6 assert "mcp_servers" in cron_cols assert "skill_packages" in { r["name"] @@ -125,12 +134,184 @@ def test_migration_002_idempotent_when_column_already_present(tmp_path: Path) -> } +def test_migration_005_preserves_populated_users_and_constraints(tmp_path: Path) -> None: + """A populated v4 DB keeps users and their foreign-key references at v5.""" + db_path = tmp_path / "octop.db" + pool = SqlitePool(db_path) + initial_migration = ( + Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" + ) + with pool.connect() as conn: + conn.executescript(initial_migration.read_text()) + conn.execute("UPDATE _schema_version SET version = 4") + conn.execute( + """ + INSERT INTO users( + id, username, password_hash, role, display_name, disabled, locale, + created_at, login_failed_count, login_locked_until, preferences_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (101, "legacy-user", "legacy-hash", "admin", "Legacy User", 1, "en", 10, 2, 20, "{}"), + ) + conn.execute( + """ + INSERT INTO agents(agent_id, user_id, name, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + """, + ("legacy-agent", 101, "Legacy Agent", 10, 20), + ) + + run_migrations(pool) + + with pool.connect() as conn: + user = conn.execute("SELECT * FROM users WHERE id = ?", (101,)).fetchone() + agent = conn.execute( + "SELECT agent_id, user_id, name FROM agents WHERE agent_id = ?", + ("legacy-agent",), + ).fetchone() + assert user is not None + assert dict(user) == { + "id": 101, + "username": "legacy-user", + "password_hash": "legacy-hash", + "role": "admin", + "display_name": "Legacy User", + "disabled": 1, + "locale": "en", + "created_at": 10, + "login_failed_count": 2, + "login_locked_until": 20, + "preferences_json": "{}", + "email": None, + "sso_provider_id": None, + "sso_subject": None, + "permissions": "[]", + } + assert dict(agent) == { + "agent_id": "legacy-agent", + "user_id": 101, + "name": "Legacy Agent", + } + assert conn.execute("PRAGMA foreign_key_check").fetchall() == [] + user_indexes = { + row["name"]: row["partial"] for row in conn.execute("PRAGMA index_list(users)") + } + assert user_indexes["idx_users_email"] == 1 + assert user_indexes["idx_users_sso"] == 1 + user_foreign_keys = conn.execute("PRAGMA foreign_key_list(users)").fetchall() + assert any( + row["from"] == "sso_provider_id" and row["table"] == "sso_providers" + for row in user_foreign_keys + ) + with pytest.raises(sqlite3.IntegrityError): + conn.execute( + """ + INSERT INTO agents(agent_id, user_id, name, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + """, + ("invalid-agent", 999, "Invalid Agent", 10, 20), + ) + + conn.execute( + "INSERT INTO users(username, password_hash, role, created_at, email) VALUES (?, ?, ?, ?, ?)", + ("passwordless", None, "user", 30, "person@example.com"), + ) + with pytest.raises(sqlite3.IntegrityError): + conn.execute( + "INSERT INTO users(username, password_hash, role, created_at, email) VALUES (?, ?, ?, ?, ?)", + ("duplicate-email", None, "user", 30, "person@example.com"), + ) + + conn.execute( + """ + INSERT INTO sso_providers( + id, enabled, display_name, issuer, client_id, scopes, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + (1, 1, "SSO", "https://issuer.example", "octop-client", "openid", 30, 30), + ) + conn.execute( + """ + INSERT INTO users(username, password_hash, role, created_at, sso_provider_id, sso_subject) + VALUES (?, ?, ?, ?, ?, ?) + """, + ("sso-user", None, "user", 30, 1, "subject-1"), + ) + with pytest.raises(sqlite3.IntegrityError): + conn.execute( + """ + INSERT INTO users(username, password_hash, role, created_at, sso_provider_id, sso_subject) + VALUES (?, ?, ?, ?, ?, ?) + """, + ("duplicate-sso", None, "user", 30, 1, "subject-1"), + ) + + +def test_stuck_version_6_without_permissions_column_is_repaired(tmp_path: Path) -> None: + """Pre-squash version clamp can leave schema at 6 without users.permissions.""" + db_path = tmp_path / "octop.db" + pool = SqlitePool(db_path) + with pool.connect() as conn: + conn.executescript( + ( + Path(__file__).resolve().parents[3] + / "src/octop/infra/db/migrations/001_initial.sql" + ).read_text() + ) + conn.execute("UPDATE _schema_version SET version = 6") + run_migrations(pool) + with pool.connect() as conn: + cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)").fetchall()} + version = conn.execute("SELECT version FROM _schema_version").fetchone()[0] + assert version == 6 + assert "permissions" in cols + + def test_foreign_keys_enabled(db: SqlitePool): with db.connect() as conn: fk = conn.execute("PRAGMA foreign_keys").fetchone()[0] assert fk == 1 +def test_pre_squash_schema_version_clamped_and_knowledge_tables_filled( + tmp_path: Path, +) -> None: + """Develop DBs that applied split 005–009 must clamp to consolidated v5.""" + db_path = tmp_path / "octop.db" + pool = SqlitePool(db_path) + with pool.connect() as conn: + conn.executescript( + ( + Path(__file__).resolve().parents[3] + / "src/octop/infra/db/migrations/001_initial.sql" + ).read_text() + ) + # Simulate a fully-applied pre-squash develop install (version 9) that + # somehow lost knowledge tables after a partial local upgrade. + conn.execute("UPDATE _schema_version SET version = 9") + conn.execute("ALTER TABLE agents ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0") + + run_migrations(pool) + + with pool.connect() as conn: + version = conn.execute("SELECT version FROM _schema_version").fetchone()[0] + tables = { + row["name"] + for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + } + user_cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)").fetchall()} + assert version == 6 + assert "permissions" in user_cols + assert { + "published_experts", + "sso_providers", + "sso_login_states", + "knowledge_bases", + "knowledge_base_members", + "knowledge_documents", + }.issubset(tables) + + def test_transaction_rolls_back_on_exception(db: SqlitePool): with pytest.raises(RuntimeError), db.transaction() as conn: conn.execute( diff --git a/tests/unit/db/test_published_experts_repo.py b/tests/unit/db/test_published_experts_repo.py new file mode 100644 index 00000000..fc5a6927 --- /dev/null +++ b/tests/unit/db/test_published_experts_repo.py @@ -0,0 +1,107 @@ +"""Unit tests for PublishedExpertRepo and migration 006.""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.published_experts import PublishedExpertRepo + + +@pytest.fixture +def db(tmp_path: Path) -> SqlitePool: + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + return pool + + +def test_published_experts_table_exists(db: SqlitePool) -> None: + with db.connect() as conn: + names = { + r["name"] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + } + v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] + assert "published_experts" in names + assert v == 6 + + +def test_published_expert_repo_create_get_list_delete(db: SqlitePool) -> None: + repo = PublishedExpertRepo(db) + row = repo.create( + id="01TESTEXPERT000000000000", + slug="office-helper", + name="Office Helper", + description="Helps with office tasks", + created_by="user-1", + source_agent_id="agent-abc", + icon_name="briefcase", + color="#336699", + ) + assert row.slug == "office-helper" + assert row.name == "Office Helper" + assert row.source_agent_id == "agent-abc" + assert row.icon_name == "briefcase" + assert row.color == "#336699" + + fetched = repo.get(row.id) + assert fetched is not None + assert fetched.slug == "office-helper" + + by_slug = repo.get_by_slug("office-helper") + assert by_slug is not None + assert by_slug.id == row.id + + listed = repo.list_all() + assert len(listed) == 1 + assert listed[0].id == row.id + + repo.delete(row.id) + assert repo.get(row.id) is None + assert repo.list_all() == [] + + +def test_published_expert_repo_unique_slug(db: SqlitePool) -> None: + repo = PublishedExpertRepo(db) + repo.create( + id="01TESTEXPERT000000000001", + slug="duplicate-slug", + name="First", + created_by="user-1", + ) + with pytest.raises(sqlite3.IntegrityError): + repo.create( + id="01TESTEXPERT000000000002", + slug="duplicate-slug", + name="Second", + created_by="user-2", + ) + + +def test_published_expert_repo_get_by_source_and_update_meta(db: SqlitePool) -> None: + repo = PublishedExpertRepo(db) + row = repo.create( + id="01TESTEXPERT000000000003", + slug="from-source", + name="From Source", + created_by="user-1", + source_agent_id="agent-xyz", + icon_name="old", + color="#111111", + ) + assert repo.get_by_source_agent_id("agent-xyz") is not None + assert repo.get_by_source_agent_id("agent-xyz").id == row.id + assert repo.get_by_source_agent_id("missing") is None + + updated = repo.update_snapshot_meta( + row.id, + icon_name="search", + color="#abcdef", + ) + assert updated.icon_name == "search" + assert updated.color == "#abcdef" + assert updated.updated_at >= row.updated_at + assert updated.updated_at >= row.created_at diff --git a/tests/unit/db/test_repo_knowledge.py b/tests/unit/db/test_repo_knowledge.py new file mode 100644 index 00000000..4b34a14b --- /dev/null +++ b/tests/unit/db/test_repo_knowledge.py @@ -0,0 +1,146 @@ +"""Unit tests for KnowledgeRepo and migration 005.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.knowledge import KnowledgeRepo +from octop.infra.db.repos.users import UserRepo +from octop.infra.utils.paths import PathLayout + + +@pytest.fixture +def db(tmp_path: Path) -> SqlitePool: + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + return pool + + +@pytest.fixture +def repo(db: SqlitePool) -> KnowledgeRepo: + return KnowledgeRepo(db) + + +@pytest.fixture +def owner_id(db: SqlitePool) -> int: + return UserRepo(db).create(username="owner", password_hash="h", role="user") + + +def test_knowledge_create_with_icon(repo: KnowledgeRepo, owner_id: int) -> None: + row = repo.create_base(owner_user_id=owner_id, name="Science", icon_name="flask-conical") + assert len(row.id) == 6 + assert row.icon_name == "flask-conical" + repo.update_base(row.id, icon_name="cpu") + updated = repo.get_base(row.id) + assert updated is not None + assert updated.icon_name == "cpu" + + +def test_knowledge_tables_migrated(db: SqlitePool) -> None: + with db.connect() as conn: + names = { + r["name"] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + } + v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] + assert { + "knowledge_bases", + "knowledge_base_members", + "knowledge_documents", + }.issubset(names) + assert v == 6 + + +def test_path_layout_knowledge_dir(tmp_path: Path) -> None: + paths = PathLayout(tmp_path / ".octop") + assert paths.knowledge_dir == tmp_path / ".octop" / "knowledge" + + +def test_knowledge_migrate_and_create( + repo: KnowledgeRepo, owner_id: int, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + paths = PathLayout.from_env() + assert paths.knowledge_dir == tmp_path / "knowledge" + + row = repo.create_base(owner_user_id=owner_id, name="Docs", description="team docs") + assert row.id + assert row.owner_user_id == owner_id + assert row.name == "Docs" + assert row.description == "team docs" + assert row.default_open is False + assert repo.count_documents(row.id) == 0 + + +def test_list_visible_owner_and_shared_base( + repo: KnowledgeRepo, db: SqlitePool, owner_id: int +) -> None: + users = UserRepo(db) + member_id = users.create(username="member", password_hash="h", role="user") + other_id = users.create(username="other", password_hash="h", role="user") + + kb = repo.create_base(owner_user_id=owner_id, name="Shared", shared=True) + repo.set_member(kb.id, user_id=member_id, role="viewer") + + owner_visible = {r.id for r in repo.list_visible(owner_id)} + member_visible = {r.id for r in repo.list_visible(member_id)} + other_visible = {r.id for r in repo.list_visible(other_id)} + + assert kb.id in owner_visible + assert kb.id in member_visible + assert kb.id in other_visible + assert kb.shared is True + + +def test_knowledge_document_crud(repo: KnowledgeRepo, owner_id: int) -> None: + kb = repo.create_base(owner_user_id=owner_id, name="Docs") + doc = repo.create_document( + kb_id=kb.id, + filename="readme.md", + content_type="text/markdown", + byte_size=42, + content_hash="abc", + ) + assert doc.status == "pending" + assert repo.count_documents(kb.id) == 1 + + listed = repo.list_documents(kb.id) + assert len(listed) == 1 + assert listed[0].id == doc.id + + repo.update_document(doc.id, status="ready", chunk_count=3) + updated = repo.get_document(doc.id) + assert updated is not None + assert updated.status == "ready" + assert updated.chunk_count == 3 + + repo.delete_document(doc.id) + assert repo.get_document(doc.id) is None + assert repo.count_documents(kb.id) == 0 + + +def test_create_document_applies_limit_within_insert_transaction( + repo: KnowledgeRepo, owner_id: int +) -> None: + kb = repo.create_base(owner_user_id=owner_id, name="Docs") + repo.create_document( + kb_id=kb.id, + filename="first.md", + content_type="text/markdown", + byte_size=1, + max_documents=1, + ) + + with pytest.raises(ValueError, match="at most 1"): + repo.create_document( + kb_id=kb.id, + filename="second.md", + content_type="text/markdown", + byte_size=1, + max_documents=1, + ) + + assert repo.count_documents(kb.id) == 1 diff --git a/tests/unit/db/test_repo_sso.py b/tests/unit/db/test_repo_sso.py new file mode 100644 index 00000000..86d95477 --- /dev/null +++ b/tests/unit/db/test_repo_sso.py @@ -0,0 +1,108 @@ +"""Tests for OIDC SSO repository access.""" + +from __future__ import annotations + +import time +from pathlib import Path + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.sso import SsoRepo + + +@pytest.fixture +def db(tmp_path: Path) -> SqlitePool: + pool = SqlitePool(tmp_path / "sso.db") + run_migrations(pool) + return pool + + +def test_login_code_single_consume(db: SqlitePool) -> None: + repo = SsoRepo(db) + now = int(time.time()) + provider = repo.upsert_provider( + enabled=True, + display_name="OneID", + issuer="https://idp.example.com", + client_id="client-id", + client_secret_enc=b"encrypted-secret", + scopes="openid profile email", + dashboard_origin="https://octop.example.com", + ) + repo.create_login_state( + state="state-1", + provider_id=provider.id, + nonce="nonce-1", + code_verifier="verifier-1", + redirect_after="/chat", + expires_at=now + 60, + ) + repo.attach_login_code( + "state-1", + login_code="login-code-1", + user_id=1, + expires_at=now + 60, + ) + + assert repo.consume_login_code("login-code-1") == {"user_id": 1} + assert repo.consume_login_code("login-code-1") is None + + +def test_provider_state_lookup_and_expiry_cleanup(db: SqlitePool) -> None: + repo = SsoRepo(db) + now = int(time.time()) + provider = repo.upsert_provider( + enabled=True, + display_name="OneID", + issuer="https://idp.example.com", + client_id="client-id", + client_secret_enc=b"encrypted-secret", + scopes="openid profile email", + dashboard_origin="https://octop.example.com", + ) + updated = repo.upsert_provider( + enabled=False, + display_name="Updated OneID", + issuer="https://idp.example.com", + client_id="client-id-2", + client_secret_enc=None, + scopes="openid", + dashboard_origin=None, + ) + repo.create_login_state( + state="active-state", + provider_id=provider.id, + nonce="nonce-1", + code_verifier="verifier-1", + redirect_after="/chat", + expires_at=now + 60, + ) + repo.create_login_state( + state="expired-state", + provider_id=provider.id, + nonce="nonce-2", + code_verifier="verifier-2", + redirect_after="/chat", + expires_at=now, + ) + + assert repo.get_provider() == updated + assert updated.client_secret_enc == b"encrypted-secret" + claimed = repo.take_login_state("active-state") + assert claimed is not None + assert claimed.state == "active-state" + assert claimed.consumed_at is not None + assert repo.take_login_state("active-state") is None + assert repo.take_login_state("expired-state") is None + + repo.delete_expired(now) + + with db.connect() as conn: + assert ( + conn.execute( + "SELECT state FROM sso_login_states WHERE state = ?", ("expired-state",) + ).fetchone() + is None + ) diff --git a/tests/unit/db/test_skill_package_icons.py b/tests/unit/db/test_skill_package_icons.py index bdc71531..e08a3b85 100644 --- a/tests/unit/db/test_skill_package_icons.py +++ b/tests/unit/db/test_skill_package_icons.py @@ -91,7 +91,7 @@ def test_migration_002_idempotent_when_icon_columns_already_present(tmp_path: Pa "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='skill_packages'" ) } - assert v == 4 + assert v == 6 assert "icon_name" in cols assert "icon_url" in cols assert "idx_skill_packages_name" in indexes @@ -109,6 +109,6 @@ def test_repair_legacy_schema_adds_icon_columns_at_version_2(tmp_path: Path) -> with pool.connect() as conn: v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] cols = {r["name"] for r in conn.execute("PRAGMA table_info(skill_packages)").fetchall()} - assert v == 4 + assert v == 6 assert "icon_name" in cols assert "icon_url" in cols diff --git a/tests/unit/db/test_skill_packages_repo.py b/tests/unit/db/test_skill_packages_repo.py index 8bf62c0c..e20e6139 100644 --- a/tests/unit/db/test_skill_packages_repo.py +++ b/tests/unit/db/test_skill_packages_repo.py @@ -25,7 +25,7 @@ def test_skill_packages_table_exists(db: SqlitePool) -> None: } v = conn.execute("SELECT version FROM _schema_version").fetchone()[0] assert "skill_packages" in names - assert v == 4 + assert v == 6 def test_skill_package_repo_create_get(db: SqlitePool) -> None: diff --git a/tests/unit/db/test_user_permissions_column.py b/tests/unit/db/test_user_permissions_column.py new file mode 100644 index 00000000..9a595964 --- /dev/null +++ b/tests/unit/db/test_user_permissions_column.py @@ -0,0 +1,60 @@ +"""Tests for users.permissions column wiring.""" + +from __future__ import annotations + +from pathlib import Path + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.users import UserRepo, UserRow + + +def test_user_row_carries_permissions(tmp_path: Path) -> None: + db = SqlitePool(tmp_path / "octop.db") + run_migrations(db) + repo = UserRepo(db) + uid = repo.create( + username="alice", + password_hash="x", + role="user", + permissions=["browser", "users"], + ) + row = repo.get(uid) + assert row is not None + assert row.permissions == ["browser", "users"] + + +def test_set_permissions_roundtrip(tmp_path: Path) -> None: + db = SqlitePool(tmp_path / "octop.db") + run_migrations(db) + repo = UserRepo(db) + uid = repo.create(username="bob", password_hash="x", role="user") + repo.set_permissions(uid, ["providers"]) + got = repo.get(uid) + assert got is not None + assert got.permissions == ["providers"] + + +def test_from_row_accepts_missing_column_default() -> None: + class _R(dict): + def keys(self): # noqa: ANN201 + return super().keys() + + raw = _R( + id=1, + username="u", + password_hash="x", + role="user", + display_name=None, + disabled=0, + created_at=0, + locale="zh", + email=None, + sso_provider_id=None, + sso_subject=None, + preferences_json="{}", + login_failed_count=0, + login_locked_until=0, + ) + row = UserRow.from_row(raw) + assert row.permissions == [] diff --git a/tests/unit/desktop/test_setup.py b/tests/unit/desktop/test_setup.py index 3b095dac..98d0cb1b 100644 --- a/tests/unit/desktop/test_setup.py +++ b/tests/unit/desktop/test_setup.py @@ -174,8 +174,8 @@ def test_desktop_status_deps_missing() -> None: def test_python_deps_install_cmd_prefers_uv(monkeypatch) -> None: monkeypatch.setattr( - "octop.infra.desktop.setup.shutil.which", - lambda name: "/usr/bin/uv" if name == "uv" else None, + "octop.infra.utils.runtime_packages.find_uv_binary", + lambda: "/usr/bin/uv", ) monkeypatch.setattr("octop.infra.desktop.setup.sys.executable", "/venv/bin/python3") cmd = python_deps_install_cmd() @@ -185,18 +185,15 @@ def test_python_deps_install_cmd_prefers_uv(monkeypatch) -> None: def test_python_deps_install_cmd_falls_back_to_pip(monkeypatch) -> None: - monkeypatch.setattr("octop.infra.desktop.setup.shutil.which", lambda _name: None) + monkeypatch.setattr("octop.infra.utils.runtime_packages.find_uv_binary", lambda: None) monkeypatch.setattr("octop.infra.desktop.setup.sys.executable", "/venv/bin/python3") - monkeypatch.setattr( - "octop.infra.desktop.setup.importlib.util.find_spec", - lambda name: object() if name == "pip" else None, - ) cmd = python_deps_install_cmd() assert cmd == [ "/venv/bin/python3", "-m", "pip", "install", + "--disable-pip-version-check", "mss>=9.0", "pynput>=1.7", "pillow>=10.0", diff --git a/tests/unit/gateway/test_gateway_runtime_status.py b/tests/unit/gateway/test_gateway_runtime_status.py index b68d1f1f..68bb1bac 100644 --- a/tests/unit/gateway/test_gateway_runtime_status.py +++ b/tests/unit/gateway/test_gateway_runtime_status.py @@ -53,6 +53,24 @@ async def test_register_success_sets_runtime_connected(tmp_path: Path) -> None: assert status is not None assert status.connected is True assert status.reason is None + assert gw._channel_manager.add_channel.await_args is not None + assert gw._channel_manager.add_channel.await_args.kwargs["processor"] is not gw._processor + + +@pytest.mark.asyncio +async def test_register_stream_mode_uses_original_processor(tmp_path: Path) -> None: + gw = _make_gateway(tmp_path) + gw._channel_manager = MagicMock() + gw._channel_manager.add_channel = AsyncMock() + gw._channel_manager.get_channel = MagicMock(return_value=MagicMock()) + gw._processor = MagicMock() + row = _fake_row() + row.config_json = '{"app_id":"x","app_secret":"y","response_mode":"stream"}' + + await gw._register_channel(row) + + assert gw._channel_manager.add_channel.await_args is not None + assert gw._channel_manager.add_channel.await_args.kwargs["processor"] is gw._processor @pytest.mark.asyncio diff --git a/tests/unit/gateway/test_processor_default_open.py b/tests/unit/gateway/test_processor_default_open.py index 3ecfe3be..ccba5987 100644 --- a/tests/unit/gateway/test_processor_default_open.py +++ b/tests/unit/gateway/test_processor_default_open.py @@ -2,6 +2,7 @@ from __future__ import annotations +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -119,6 +120,73 @@ async def test_dashboard_request_trusts_explicit_opt_out() -> None: agent_manager.merge_turn_mcp_servers.assert_called_once_with(1, [], apply_defaults=False) +@pytest.mark.asyncio +async def test_dashboard_request_attaches_knowledge_base_ids_without_prepending() -> None: + agent_manager = MagicMock() + agent_manager.merge_turn_mcp_servers = MagicMock(return_value=None) + agent_manager.get_row = MagicMock(return_value=None) + agent_manager.providers = MagicMock() + agent_manager.providers.is_model_ref_usable = MagicMock(return_value=False) + agent_manager.providers.resolve_explicit_default_model = MagicMock(return_value=None) + agent_manager.providers.resolve_model_for_multimodal_turn = MagicMock( + side_effect=lambda ref, **_k: ref + ) + agent_manager.get_thread_model = MagicMock(return_value=None) + + base = SimpleNamespace( + id="kb-1", + owner_user_id=1, + name="Refund policy", + description="Retail refund rules", + default_open=False, + shared=False, + ) + processor = GlobalProcessor( + agent_manager=agent_manager, + thread_registry=MagicMock(), + audit_repo=MagicMock(), + agent_repo=MagicMock(get=MagicMock(return_value=MagicMock(default_model=None))), + user_repo=MagicMock(), + connector_repo=MagicMock(), + knowledge_repo=MagicMock(list_visible=MagicMock(return_value=[base])), + settings_repo=MagicMock(), + dispatcher=SlashDispatcher(), + usage_repo=None, + gateway=None, + ) + msg = InboundMessage( + channel_id="ws", + channel_type="dashboard", + tenant_id="agent-1", + channel_subject=ChannelSubject(subject_id="1"), + content=[TextContent(text="question")], + metadata={"knowledge_base_ids": ["kb-1"], "user_is_admin": False}, + ) + + request = await processor._build_dashboard_request( + msg, + agent_id="agent-1", + user_id=1, + session_key="sk", + thread_id="thr", + meta=msg.metadata or {}, + ) + + content = request["messages"][0]["content"] + assert content == "question" + configurable = request.get("configurable") or {} + assert configurable["knowledge_base_ids"] == ["kb-1"] + assert configurable["knowledge_base_catalog"] == [ + { + "id": "kb-1", + "name": "Refund policy", + "description": "Retail refund rules", + } + ] + assert configurable["user_is_admin"] is False + assert "locale" in configurable + + @pytest.mark.asyncio async def test_resolve_turn_mcp_servers_raises_when_prepare_fails() -> None: agent_manager = MagicMock() diff --git a/tests/unit/gateway/test_processor_knowledge_services.py b/tests/unit/gateway/test_processor_knowledge_services.py new file mode 100644 index 00000000..764419ee --- /dev/null +++ b/tests/unit/gateway/test_processor_knowledge_services.py @@ -0,0 +1,27 @@ +"""GlobalProcessor knowledge services must carry provider_repo for remote embeds.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from octop.infra.gateway.process.processor import GlobalProcessor +from octop.infra.gateway.slash.dispatcher import SlashDispatcher + + +def test_processor_knowledge_services_include_provider_repo() -> None: + provider_repo = object() + processor = GlobalProcessor( + agent_manager=MagicMock(), + thread_registry=MagicMock(), + audit_repo=MagicMock(), + agent_repo=MagicMock(), + user_repo=MagicMock(), + connector_repo=MagicMock(), + knowledge_repo=MagicMock(), + settings_repo=MagicMock(), + provider_repo=provider_repo, + dispatcher=SlashDispatcher(), + ) + + assert processor._knowledge_services is not None + assert processor._knowledge_services.provider_repo is provider_repo diff --git a/tests/unit/gateway/test_response_mode.py b/tests/unit/gateway/test_response_mode.py new file mode 100644 index 00000000..7bbe1777 --- /dev/null +++ b/tests/unit/gateway/test_response_mode.py @@ -0,0 +1,96 @@ +"""Tests for external IM response delivery modes.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator + +import pytest +from harness_gateway.models import ( + FileContent, + InboundMessage, + MessageEvent, + MessageEventType, + TextContent, +) + +from octop.infra.gateway.process.response_mode import ( + collapse_to_invoke_response, + normalize_channel_response_mode, + processor_for_response_mode, +) + + +async def _events(*events: MessageEvent) -> AsyncIterator[MessageEvent]: + for event in events: + yield event + + +@pytest.mark.asyncio +async def test_invoke_discards_progress_before_tool_and_emits_final_once() -> None: + source = _events( + MessageEvent.typing(), + MessageEvent.delta("我先查一下。"), + MessageEvent.flush(), + MessageEvent.tool_start("web_fetch"), + MessageEvent.tool_end("web_fetch"), + MessageEvent.delta("这是"), + MessageEvent.delta("最终答案。"), + MessageEvent.completed(), + ) + + result = [event async for event in collapse_to_invoke_response(source)] + + assert [event.type for event in result] == [ + MessageEventType.MESSAGE, + MessageEventType.COMPLETED, + ] + text = result[0].content[0] + assert isinstance(text, TextContent) + assert text.text == "这是最终答案。" + + +@pytest.mark.asyncio +async def test_invoke_preserves_tool_media_with_final_text() -> None: + attachment = FileContent(filename="report.pdf", data="cGRm") + source = _events( + MessageEvent.tool_start("write_file"), + MessageEvent.tool_end("write_file"), + MessageEvent(type=MessageEventType.MESSAGE, content=[attachment]), + MessageEvent.delta("报告已生成。"), + MessageEvent.completed(), + ) + + result = [event async for event in collapse_to_invoke_response(source)] + + assert result[0].type == MessageEventType.MESSAGE + assert len(result[0].content) == 2 + assert isinstance(result[0].content[0], TextContent) + assert result[0].content[1] is attachment + + +@pytest.mark.asyncio +async def test_invoke_forwards_error_without_partial_text() -> None: + error = MessageEvent.error_event("upstream failed") + source = _events( + MessageEvent.delta("partial"), + error, + MessageEvent.completed(), + ) + + result = [event async for event in collapse_to_invoke_response(source)] + + assert result == [error, MessageEvent.completed()] + + +def test_response_mode_defaults_to_invoke_and_accepts_stream() -> None: + assert normalize_channel_response_mode(None) == "invoke" + assert normalize_channel_response_mode("unknown") == "invoke" + assert normalize_channel_response_mode(" STREAM ") == "stream" + + +def test_stream_mode_uses_original_processor() -> None: + async def processor(_message: InboundMessage) -> AsyncIterator[MessageEvent]: + yield MessageEvent.completed() + + assert processor_for_response_mode(processor, "stream") is processor + assert processor_for_response_mode(processor, "invoke") is not processor diff --git a/tests/unit/gateway/test_slash_compact.py b/tests/unit/gateway/test_slash_compact.py index 2b747988..306a211e 100644 --- a/tests/unit/gateway/test_slash_compact.py +++ b/tests/unit/gateway/test_slash_compact.py @@ -80,14 +80,16 @@ async def test_cmd_compact_calls_harness_without_reset(ctx, dispatcher) -> None: ) ) ctx.agent_manager.get_agent = MagicMock(return_value=harness) - before = len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id)) + before = len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id)) sink = BufferSink() await cmd_compact(dispatcher, SlashCommand("compact", ""), ctx, sink) harness.acompact_conversation.assert_awaited_once() assert harness.acompact_conversation.await_args.args[0] == tid - assert len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id)) == before + assert ( + len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id)) == before + ) assert ctx.thread_registry.get_bound_thread_id(ctx.session_key) == tid text = "\n".join(sink.lines) assert "12" in text diff --git a/tests/unit/gateway/test_slash_dispatcher.py b/tests/unit/gateway/test_slash_dispatcher.py index 2d8bc701..ad7abee3 100644 --- a/tests/unit/gateway/test_slash_dispatcher.py +++ b/tests/unit/gateway/test_slash_dispatcher.py @@ -82,7 +82,7 @@ async def test_new_creates_thread(dispatcher, ctx): channel_type=ctx.channel_type, ) await dispatcher.handle(SlashCommand("new", ""), ctx, sink) - assert len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id)) >= 2 + assert len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id)) >= 2 async def test_clear_is_alias_of_new(dispatcher, ctx): @@ -93,9 +93,12 @@ async def test_clear_is_alias_of_new(dispatcher, ctx): user_id=ctx.user_id, channel_type=ctx.channel_type, ) - before = len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id)) + before = len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id)) await dispatcher.handle(SlashCommand("clear", ""), ctx, sink) - assert len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id)) == before + 1 + assert ( + len(ctx.thread_registry.list_threads(agent_id=ctx.agent_id, user_id=ctx.user_id)) + == before + 1 + ) async def test_list_returns_threads(dispatcher, ctx): diff --git a/tests/unit/gateway/test_thread_registry.py b/tests/unit/gateway/test_thread_registry.py index 2199413b..adc5be97 100644 --- a/tests/unit/gateway/test_thread_registry.py +++ b/tests/unit/gateway/test_thread_registry.py @@ -62,7 +62,7 @@ async def test_reset_creates_new(registry: ThreadRegistry) -> None: channel_chat_type="dm", ) assert tid1 != tid2 - assert len(registry.list_threads(agent_id="a1")) == 2 + assert len(registry.list_threads(agent_id="a1", user_id=1)) == 2 row = registry.get_thread(tid2) assert row is not None assert row.last_active == 0 diff --git a/tests/unit/i18n/test_tools.py b/tests/unit/i18n/test_tools.py index d73c60d7..bd57bd7d 100644 --- a/tests/unit/i18n/test_tools.py +++ b/tests/unit/i18n/test_tools.py @@ -5,7 +5,7 @@ import json from pathlib import Path -from octop.i18n import all_tool_labels, tool_display_name +from octop.i18n import all_tool_labels, hitl_tool_catalog, tool_display_name def test_tool_display_name_known_zh(): @@ -27,6 +27,32 @@ def test_all_tool_labels_includes_unknown(): assert "unknown" in labels +def test_hitl_tool_catalog_excludes_must_use_tools(): + names = {entry.name for entry in hitl_tool_catalog()} + assert "unknown" not in names + assert "current_time" not in names + assert "write_todos" not in names + assert "memory_search" not in names + assert "search_knowledge" not in names + assert "cronjob_create" not in names + assert "task" not in names + assert "ask_agent" not in names + + +def test_hitl_tool_catalog_includes_common_tools(): + names = {entry.name for entry in hitl_tool_catalog()} + assert "bash" in names + assert "write_file" in names + assert "browser_use" in names + assert "web_fetch" in names + + +def test_hitl_tool_catalog_bilingual_labels(): + entry = next(e for e in hitl_tool_catalog() if e.name == "read_file") + assert entry.label_zh == "读取文件" + assert entry.label_en == "Read file" + + def test_dashboard_tools_match_backend(): repo = Path(__file__).resolve().parents[3] dash_en = json.loads((repo / "dashboard/src/locales/en.json").read_text(encoding="utf-8")) diff --git a/tests/unit/knowledge/test_chunk.py b/tests/unit/knowledge/test_chunk.py new file mode 100644 index 00000000..0f54d63f --- /dev/null +++ b/tests/unit/knowledge/test_chunk.py @@ -0,0 +1,19 @@ +"""Unit tests for document chunking.""" + +from __future__ import annotations + +import pytest + +from octop.infra.knowledge.chunk import chunk_text + + +def test_chunk_text_preserves_content_with_configured_overlap() -> None: + chunks = chunk_text("abcdefghij", size=4, overlap=1) + + assert chunks == ["abcd", "defg", "ghij"] + + +def test_chunk_text_strips_empty_content_and_rejects_invalid_window() -> None: + assert chunk_text(" \n\t ") == [] + with pytest.raises(ValueError, match="overlap"): + chunk_text("text", size=4, overlap=4) diff --git a/tests/unit/knowledge/test_embed.py b/tests/unit/knowledge/test_embed.py new file mode 100644 index 00000000..f05ed9f8 --- /dev/null +++ b/tests/unit/knowledge/test_embed.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from octop.infra.knowledge import embed + + +def test_remote_embedding_routes_to_provider_embeddings_endpoint(monkeypatch) -> None: + provider = SimpleNamespace(base_url="https://example.test/v1/", api_key="secret") + services = SimpleNamespace( + settings_repo=SimpleNamespace( + get=lambda key: { + "knowledge_embedding_backend": "remote", + "knowledge_embedding_model": "embed-1", + "knowledge_embedding_provider_id": "7", + }.get(key) + ), + provider_repo=SimpleNamespace( + get=lambda provider_id: provider if provider_id == 7 else None + ), + ) + captured = {} + + class Response: + def raise_for_status(self): + return None + + def json(self): + return {"data": [{"embedding": [1.0, 2.0]}]} + + class Client: + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def post(self, url, *, headers, json): + captured.update(url=url, headers=headers, json=json) + return Response() + + monkeypatch.setattr(embed.httpx, "Client", lambda **_kwargs: Client()) + + assert embed.embed_knowledge_texts(services, ["hello"]) == [[1.0, 2.0]] + assert captured["url"] == "https://example.test/v1/embeddings" + assert captured["json"] == {"model": "embed-1", "input": ["hello"]} + assert captured["headers"]["Authorization"] == "Bearer secret" + + +def test_remote_embedding_merges_provider_extra_headers(monkeypatch) -> None: + provider = SimpleNamespace( + base_url="https://example.test/v1/", + api_key="secret", + extra_json='{"headers": {"X-Custom": "1"}}', + ) + services = SimpleNamespace( + settings_repo=SimpleNamespace( + get=lambda key: { + "knowledge_embedding_backend": "remote", + "knowledge_embedding_model": "embed-1", + "knowledge_embedding_provider_id": "7", + }.get(key) + ), + provider_repo=SimpleNamespace( + get=lambda provider_id: provider if provider_id == 7 else None + ), + ) + captured = {} + + class Response: + def raise_for_status(self): + return None + + def json(self): + return {"data": [{"embedding": [0.5]}]} + + class Client: + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def post(self, url, *, headers, json): + captured.update(url=url, headers=headers, json=json) + return Response() + + monkeypatch.setattr(embed.httpx, "Client", lambda **_kwargs: Client()) + + assert embed.embed_knowledge_texts(services, ["hello"]) == [[0.5]] + assert captured["headers"]["Authorization"] == "Bearer secret" + assert captured["headers"]["X-Custom"] == "1" diff --git a/tests/unit/knowledge/test_gate.py b/tests/unit/knowledge/test_gate.py new file mode 100644 index 00000000..55803137 --- /dev/null +++ b/tests/unit/knowledge/test_gate.py @@ -0,0 +1,119 @@ +"""Unit tests for the knowledge-base capability gate.""" + +from __future__ import annotations + +import pytest + +from octop.infra.knowledge import gate + + +def test_capability_is_disabled_by_default(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(gate, "local_embedding_deps_available", lambda: True) + monkeypatch.setattr(gate, "is_model_downloaded", lambda _model: True) + + capability = gate.get_capability(lambda _key: None) + + assert capability["feature_enabled"] is False + assert capability["selected_model"] == "" + assert capability["prerequisites_ok"] is False + assert capability["usable"] is False + assert capability["checks"] == { + "model_selected": False, + "model_downloaded": False, + "deps_available": True, + "provider_ready": False, + } + + +def test_enabled_capability_uses_selected_model_not_onnx_service( + monkeypatch: pytest.MonkeyPatch, +) -> None: + model = "BAAI/bge-small-zh-v1.5" + monkeypatch.setattr( + gate, + "embedding_prerequisites_ok_for_model", + lambda candidate: candidate == model, + ) + monkeypatch.setattr(gate, "is_model_downloaded", lambda candidate: candidate == model) + monkeypatch.setattr(gate, "local_embedding_deps_available", lambda: True) + + values = { + "knowledge_bases_enabled": "true", + "knowledge_embedding_model": model, + "onnx_local_service": '{"enabled": false}', + } + capability = gate.get_capability(values.get) + + assert capability["feature_enabled"] is True + assert capability["selected_model"] == model + assert capability["prerequisites_ok"] is True + assert capability["usable"] is True + + +def test_enabling_requires_model_and_persists_verified_selection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + values: dict[str, str] = {} + model = "BAAI/bge-small-zh-v1.5" + monkeypatch.setattr( + gate, + "require_embedding_prerequisites_for_model", + lambda candidate: candidate.strip(), + ) + + with pytest.raises(ValueError, match="requires an embedding model"): + gate.set_feature_enabled(values.get, values.__setitem__, enabled=True, model=None) + + gate.set_feature_enabled(values.get, values.__setitem__, enabled=True, model=model) + + assert values == { + "knowledge_bases_enabled": "true", + "knowledge_embedding_backend": "onnx", + "knowledge_embedding_model": model, + "knowledge_embedding_provider_id": "", + } + + +def test_assert_knowledge_usable_distinguishes_disabled_from_prerequisites( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(gate, "embedding_prerequisites_ok_for_model", lambda _model: False) + with pytest.raises(RuntimeError, match="disabled"): + gate.assert_knowledge_usable(lambda _key: None) + + settings = { + "knowledge_bases_enabled": "true", + "knowledge_embedding_model": "BAAI/bge-small-zh-v1.5", + } + with pytest.raises(RuntimeError, match="prerequisites"): + gate.assert_knowledge_usable(settings.get) + + +def test_remote_capability_requires_enabled_embedding_provider( + monkeypatch: pytest.MonkeyPatch, +) -> None: + provider = type( + "Provider", + (), + { + "enabled": True, + "api_key": "key", + "base_url": "https://example.test/v1", + "name": "Remote", + "get_models": lambda self: [{"id": "embed-1", "embedding": True}], + }, + )() + monkeypatch.setattr(gate, "_provider_for_settings", lambda _repo, _id: provider) + + values = { + "knowledge_bases_enabled": "true", + "knowledge_embedding_backend": "remote", + "knowledge_embedding_model": "embed-1", + "knowledge_embedding_provider_id": "7", + } + capability = gate.get_capability(values.get) + + assert capability["backend"] == "remote" + assert capability["provider_id"] == "7" + assert capability["prerequisites_ok"] is True + assert capability["checks"]["provider_ready"] is True diff --git a/tests/unit/knowledge/test_index.py b/tests/unit/knowledge/test_index.py new file mode 100644 index 00000000..6b9c1005 --- /dev/null +++ b/tests/unit/knowledge/test_index.py @@ -0,0 +1,31 @@ +"""Unit tests for per-knowledge-base sidecar indexes.""" + +from __future__ import annotations + +from octop.infra.knowledge.index import KnowledgeIndex + + +def test_index_replaces_document_chunks_and_returns_cosine_top_k(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path)) + index = KnowledgeIndex("kb-1") + + index.replace_doc_chunks( + "doc-1", + ["first", "second"], + [[1.0, 0.0], [0.0, 1.0]], + metadata=[{"page": 1}, {"page": 2}], + ) + index.replace_doc_chunks("doc-2", ["third"], [[0.8, 0.2]]) + + hits = index.search([1.0, 0.0], k=2) + + assert [hit.text for hit in hits] == ["first", "third"] + assert hits[0].doc_id == "doc-1" + assert hits[0].ordinal == 0 + assert hits[0].metadata == {"page": 1} + + index.replace_doc_chunks("doc-1", ["replacement"], [[0.0, 1.0]]) + assert [hit.text for hit in index.search([1.0, 0.0], k=5)] == ["third", "replacement"] + + index.delete_doc("doc-2") + assert [hit.doc_id for hit in index.search([1.0, 0.0], k=5)] == ["doc-1"] diff --git a/tests/unit/knowledge/test_jobs.py b/tests/unit/knowledge/test_jobs.py new file mode 100644 index 00000000..b7dc0ab4 --- /dev/null +++ b/tests/unit/knowledge/test_jobs.py @@ -0,0 +1,171 @@ +"""Unit tests for in-process document indexing jobs.""" + +from __future__ import annotations + +import asyncio +import threading +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.knowledge import KnowledgeRepo +from octop.infra.db.repos.settings import SettingsRepo +from octop.infra.db.repos.users import UserRepo +from octop.infra.knowledge import jobs +from octop.infra.knowledge.files import write_document +from octop.infra.knowledge.index import KnowledgeIndex + + +def test_enqueue_index_document_limits_concurrency(monkeypatch: pytest.MonkeyPatch) -> None: + jobs.reset_index_semaphore_for_tests() + monkeypatch.setattr(jobs, "INDEX_CONCURRENCY", 1) + + lock = threading.Lock() + active = 0 + peak = 0 + entered = threading.Event() + hold = threading.Event() + + def blocking(_services: object, _kb: str, _doc: str) -> None: + nonlocal active, peak + with lock: + active += 1 + peak = max(peak, active) + entered.set() + assert hold.wait(timeout=2) + with lock: + active -= 1 + + monkeypatch.setattr(jobs, "process_document", blocking) + + async def run() -> None: + first = jobs.enqueue_index_document(object(), "kb", "d1") + second = jobs.enqueue_index_document(object(), "kb", "d2") + for _ in range(100): + if entered.is_set(): + break + await asyncio.sleep(0.01) + await asyncio.sleep(0.05) + assert peak == 1 + hold.set() + await asyncio.gather(first, second) + + asyncio.run(run()) + assert peak == 1 + + +def test_process_document_marks_empty_extract_as_failed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path / "home")) + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + repo = KnowledgeRepo(pool) + owner_id = UserRepo(pool).create(username="owner", password_hash="h", role="user") + kb = repo.create_base( + owner_user_id=owner_id, name="Docs", embedding_model="BAAI/bge-small-zh-v1.5" + ) + doc = repo.create_document( + kb_id=kb.id, filename="blank.md", content_type="text/markdown", byte_size=0 + ) + write_document(kb.id, doc.id, doc.filename, b" \n") + services = SimpleNamespace(knowledge_repo=repo, settings_repo=SettingsRepo(pool)) + monkeypatch.setattr(jobs, "assert_knowledge_usable", lambda *_args: None) + monkeypatch.setattr( + jobs, "embed_knowledge_texts", lambda *_a, **_k: (_ for _ in ()).throw(AssertionError()) + ) + + with pytest.raises(ValueError, match="no extractable text"): + jobs.process_document(services, kb.id, doc.id) + + updated = repo.get_document(doc.id) + assert updated is not None + assert updated.status == "failed" + assert updated.chunk_count == 0 + + +def test_process_document_indexes_chunks_and_marks_ready( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path / "home")) + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + repo = KnowledgeRepo(pool) + owner_id = UserRepo(pool).create(username="owner", password_hash="h", role="user") + kb = repo.create_base( + owner_user_id=owner_id, name="Docs", embedding_model="BAAI/bge-small-zh-v1.5" + ) + doc = repo.create_document( + kb_id=kb.id, filename="notes.md", content_type="text/markdown", byte_size=5 + ) + write_document(kb.id, doc.id, doc.filename, b"hello") + services = SimpleNamespace(knowledge_repo=repo, settings_repo=SettingsRepo(pool)) + monkeypatch.setattr(jobs, "assert_knowledge_usable", lambda *_args: None) + monkeypatch.setattr( + jobs, "embed_knowledge_texts", lambda _services, texts: [[1.0, 0.0] for _ in texts] + ) + + jobs.process_document(services, kb.id, doc.id) + + updated = repo.get_document(doc.id) + assert updated is not None + assert updated.status == "ready" + assert updated.chunk_count == 1 + assert KnowledgeIndex(kb.id).search([1.0, 0.0], 1)[0].doc_id == doc.id + + +def test_reindex_all_updates_base_model_marks_documents_pending_and_enqueues( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + repo = KnowledgeRepo(pool) + owner_id = UserRepo(pool).create(username="owner", password_hash="h", role="user") + kb = repo.create_base(owner_user_id=owner_id, name="Docs", embedding_model="old-model") + doc = repo.create_document( + kb_id=kb.id, filename="notes.md", content_type="text/markdown", byte_size=5 + ) + repo.update_document(doc.id, status="ready", chunk_count=1) + services = SimpleNamespace(knowledge_repo=repo) + enqueued: list[tuple[str, str]] = [] + monkeypatch.setattr( + jobs, + "enqueue_index_document", + lambda _services, kb_id, doc_id: enqueued.append((kb_id, doc_id)), + ) + + jobs.reindex_all_documents(services, "new-model") + + assert repo.get_base(kb.id).embedding_model == "new-model" + assert repo.get_base(kb.id).embedding_dim == 0 + assert repo.get_document(doc.id).status == "pending" + assert enqueued == [(kb.id, doc.id)] + + +def test_resume_pending_index_jobs_resets_processing_and_enqueues( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + repo = KnowledgeRepo(pool) + owner_id = UserRepo(pool).create(username="owner", password_hash="h", role="user") + kb = repo.create_base(owner_user_id=owner_id, name="Docs") + doc = repo.create_document( + kb_id=kb.id, filename="notes.md", content_type="text/markdown", byte_size=5 + ) + repo.update_document(doc.id, status="processing") + services = SimpleNamespace(knowledge_repo=repo) + enqueued: list[tuple[str, str]] = [] + monkeypatch.setattr( + jobs, + "enqueue_index_document", + lambda _services, kb_id, doc_id: enqueued.append((kb_id, doc_id)), + ) + + jobs.resume_pending_index_jobs(services) + + assert repo.get_document(doc.id).status == "pending" + assert enqueued == [(kb.id, doc.id)] diff --git a/tests/unit/knowledge/test_knowledge_default_open.py b/tests/unit/knowledge/test_knowledge_default_open.py new file mode 100644 index 00000000..16da2a1e --- /dev/null +++ b/tests/unit/knowledge/test_knowledge_default_open.py @@ -0,0 +1,32 @@ +"""Unit tests for knowledge-base turn selection defaults.""" + +from __future__ import annotations + +from types import SimpleNamespace + +from octop.infra.knowledge.default_open import merge_knowledge_base_ids + + +def test_merge_knowledge_base_ids_applies_only_visible_defaults_when_omitted() -> None: + visible = [ + SimpleNamespace(id="default", owner_user_id=1, default_open=True, shared=False), + SimpleNamespace(id="optional", owner_user_id=1, default_open=False, shared=False), + ] + + assert merge_knowledge_base_ids(visible, None, owner_user_id=1) == ["default"] + assert merge_knowledge_base_ids(visible, [], owner_user_id=1) == [] + assert merge_knowledge_base_ids(visible, ["optional", "unknown"], owner_user_id=1) == [ + "optional", + "unknown", + ] + + +def test_merge_knowledge_base_ids_default_open_only_for_owner() -> None: + visible = [ + SimpleNamespace(id="mine", owner_user_id=1, default_open=True, shared=False), + SimpleNamespace(id="shared-default", owner_user_id=2, default_open=True, shared=True), + SimpleNamespace(id="shared-opt", owner_user_id=2, default_open=False, shared=True), + ] + + assert merge_knowledge_base_ids(visible, None, owner_user_id=1) == ["mine"] + assert merge_knowledge_base_ids(visible, None, owner_user_id=2) == ["shared-default"] diff --git a/tests/unit/knowledge/test_knowledge_hint.py b/tests/unit/knowledge/test_knowledge_hint.py new file mode 100644 index 00000000..370000b9 --- /dev/null +++ b/tests/unit/knowledge/test_knowledge_hint.py @@ -0,0 +1,138 @@ +"""Unit tests for knowledge-base tool description enrichment.""" + +from __future__ import annotations + +from contextlib import contextmanager +from types import SimpleNamespace +from typing import Annotated +from unittest.mock import MagicMock + +from langchain.agents.middleware import ModelRequest +from langchain_core.tools import StructuredTool +from langgraph.config import var_child_runnable_config +from pydantic import Field + +from octop.infra.knowledge.hint import ( + KnowledgeSearchHintMiddleware, + catalog_for_selected_bases, +) +from octop.infra.knowledge.tools import format_search_knowledge_description + + +@contextmanager +def _configurable(**kwargs: object): + token = var_child_runnable_config.set({"configurable": kwargs}) + try: + yield + finally: + var_child_runnable_config.reset(token) + + +def _dummy_tool(description: str = "base") -> StructuredTool: + async def search_knowledge( + query: Annotated[str, Field(description="q")], + ) -> str: + return query + + return StructuredTool.from_function( + coroutine=search_knowledge, + name="search_knowledge", + description=description, + ) + + +def test_catalog_for_selected_bases_includes_name_and_description() -> None: + bases = [ + SimpleNamespace(id="kb-1", name="Refund policy", description="Retail refund rules"), + SimpleNamespace(id="kb-2", name="", description=""), + ] + assert catalog_for_selected_bases(bases, ["kb-1", "kb-2", "missing"]) == [ + {"id": "kb-1", "name": "Refund policy", "description": "Retail refund rules"}, + {"id": "kb-2", "name": "kb-2", "description": ""}, + ] + + +def test_format_description_lists_titles_and_descriptions() -> None: + text = format_search_knowledge_description( + [ + {"id": "kb-1", "name": "Refund policy", "description": "Retail refund rules"}, + {"id": "kb-2", "name": "HR handbook", "description": ""}, + ] + ) + assert "Attached this turn:" in text + assert "- Refund policy: Retail refund rules" in text + assert "- HR handbook" in text + assert "overlaps any of those topics" in text + + +def test_format_description_clips_long_fields() -> None: + text = format_search_knowledge_description( + [{"id": "kb-1", "name": "Policy", "description": "x" * 400}] + ) + line = next(part for part in text.splitlines() if part.startswith("- Policy:")) + assert line.endswith("...") + assert len(line) < 260 + + +def test_format_description_none_selected() -> None: + text = format_search_knowledge_description([]) + assert text == "No knowledge bases are attached this turn." + + +def test_middleware_rewrites_search_knowledge_description() -> None: + other = StructuredTool.from_function( + func=lambda: "x", + name="other", + description="keep", + ) + request = ModelRequest( + model=MagicMock(), + messages=[], + tools=[_dummy_tool("stale"), other], + ) + captured: list[ModelRequest] = [] + + def handler(req: ModelRequest) -> object: + captured.append(req) + return object() + + with _configurable( + knowledge_base_catalog=[ + { + "id": "kb-1", + "name": "Refund policy", + "description": "Retail refund rules", + } + ], + ): + KnowledgeSearchHintMiddleware().wrap_model_call(request, handler) + + tools = list(captured[0].tools or []) + kb_tool = next(t for t in tools if getattr(t, "name", None) == "search_knowledge") + assert "Refund policy: Retail refund rules" in kb_tool.description + assert next(t for t in tools if getattr(t, "name", None) == "other").description == "keep" + assert request.tools[0].description == "stale" + + +def test_middleware_hides_tool_when_catalog_empty() -> None: + other = StructuredTool.from_function( + func=lambda: "x", + name="other", + description="keep", + ) + request = ModelRequest( + model=MagicMock(), + messages=[], + tools=[_dummy_tool("stale"), other], + ) + captured: list[ModelRequest] = [] + + def handler(req: ModelRequest) -> object: + captured.append(req) + return object() + + with _configurable(knowledge_base_catalog=[]): + KnowledgeSearchHintMiddleware().wrap_model_call(request, handler) + + names = [getattr(t, "name", None) for t in (captured[0].tools or [])] + assert names == ["other"] diff --git a/tests/unit/knowledge/test_knowledge_tools.py b/tests/unit/knowledge/test_knowledge_tools.py new file mode 100644 index 00000000..6212535e --- /dev/null +++ b/tests/unit/knowledge/test_knowledge_tools.py @@ -0,0 +1,96 @@ +"""Unit tests for built-in knowledge-base LangChain tools.""" + +from __future__ import annotations + +from contextlib import contextmanager +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from langgraph.config import var_child_runnable_config + +from octop.infra.knowledge.tools import build_knowledge_tools + + +@contextmanager +def _configurable(**kwargs: object): + token = var_child_runnable_config.set({"configurable": kwargs}) + try: + yield + finally: + var_child_runnable_config.reset(token) + + +def _tool_by_name(tools: list, name: str): + for tool in tools: + if tool.name == name: + return tool + raise KeyError(name) + + +def test_search_knowledge_default_description_is_empty_catalog_fallback() -> None: + tool = _tool_by_name(build_knowledge_tools(SimpleNamespace()), "search_knowledge") + assert tool.description == "No knowledge bases are attached this turn." + + +@pytest.mark.asyncio +async def test_search_knowledge_requires_selected_bases() -> None: + services = SimpleNamespace( + knowledge_repo=MagicMock(), + settings_repo=MagicMock(), + provider_repo=MagicMock(), + ) + tool = _tool_by_name(build_knowledge_tools(services), "search_knowledge") + with _configurable(user="1", knowledge_base_ids=[]): + out = await tool.ainvoke({"query": "billing policy"}) + assert "No knowledge bases selected" in out + + +@pytest.mark.asyncio +async def test_search_knowledge_returns_retrieved_context() -> None: + services = SimpleNamespace( + knowledge_repo=MagicMock(), + settings_repo=MagicMock(), + provider_repo=MagicMock(), + ) + tool = _tool_by_name(build_knowledge_tools(services), "search_knowledge") + retrieved = AsyncMock(return_value="[KB: Docs]\nuseful passage") + with ( + _configurable( + user="7", + user_is_admin=False, + knowledge_base_ids=["kb-1"], + locale="zh", + ), + patch("octop.infra.knowledge.tools.retrieve_context", retrieved), + ): + out = await tool.ainvoke({"query": "refund rules", "k": 3}) + + assert out == "[KB: Docs]\nuseful passage" + retrieved.assert_awaited_once() + kwargs = retrieved.await_args.kwargs + assert kwargs["user_id"] == 7 + assert kwargs["is_admin"] is False + assert kwargs["query"] == "refund rules" + assert kwargs["knowledge_base_ids"] == ["kb-1"] + assert kwargs["k"] == 3 + assert kwargs["locale"] == "zh" + + +@pytest.mark.asyncio +async def test_search_knowledge_empty_hits_message() -> None: + services = SimpleNamespace( + knowledge_repo=MagicMock(), + settings_repo=MagicMock(), + provider_repo=MagicMock(), + ) + tool = _tool_by_name(build_knowledge_tools(services), "search_knowledge") + with ( + _configurable(user="1", knowledge_base_ids=["kb-1"]), + patch( + "octop.infra.knowledge.tools.retrieve_context", + AsyncMock(return_value=""), + ), + ): + out = await tool.ainvoke({"query": "nothing"}) + assert "No relevant passages found" in out diff --git a/tests/unit/knowledge/test_parse.py b/tests/unit/knowledge/test_parse.py new file mode 100644 index 00000000..a32a58e1 --- /dev/null +++ b/tests/unit/knowledge/test_parse.py @@ -0,0 +1,53 @@ +"""Unit tests for supported knowledge-document parsers.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from octop.infra.knowledge.parse import parse_document + + +def test_parse_plain_text_and_markdown(tmp_path: Path) -> None: + text = tmp_path / "notes.txt" + markdown = tmp_path / "readme.md" + text.write_text("plain notes", encoding="utf-8") + markdown.write_text("# Heading\n\nbody", encoding="utf-8") + + assert parse_document(text) == "plain notes" + assert parse_document(markdown) == "# Heading\n\nbody" + + +def test_parse_pdf_docx_and_pptx(tmp_path: Path) -> None: + from docx import Document + from pptx import Presentation + from pypdf import PdfWriter + + pdf = tmp_path / "empty.pdf" + writer = PdfWriter() + writer.add_blank_page(width=72, height=72) + with pdf.open("wb") as output: + writer.write(output) + + docx = tmp_path / "notes.docx" + document = Document() + document.add_paragraph("Word notes") + document.save(docx) + + pptx = tmp_path / "slides.pptx" + presentation = Presentation() + presentation.slides.add_slide(presentation.slide_layouts[0]).shapes.title.text = "Slide title" + presentation.save(pptx) + + assert parse_document(pdf) == "" + assert parse_document(docx) == "Word notes" + assert parse_document(pptx) == "Slide title" + + +def test_parse_rejects_unsupported_extension(tmp_path: Path) -> None: + path = tmp_path / "unsupported.xlsx" + path.write_bytes(b"not a spreadsheet") + + with pytest.raises(ValueError, match="unsupported"): + parse_document(path) diff --git a/tests/unit/knowledge/test_retrieve.py b/tests/unit/knowledge/test_retrieve.py new file mode 100644 index 00000000..5ca7fbfc --- /dev/null +++ b/tests/unit/knowledge/test_retrieve.py @@ -0,0 +1,79 @@ +"""Unit tests for knowledge retrieval during a chat turn.""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.knowledge import KnowledgeRepo +from octop.infra.db.repos.settings import SettingsRepo +from octop.infra.db.repos.users import UserRepo +from octop.infra.knowledge import retrieve as retrieve_module +from octop.infra.knowledge.index import KnowledgeIndex + + +def test_retrieve_context_filters_unreadable_knowledge_base(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path / "home")) + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + services = SimpleNamespace( + knowledge_repo=KnowledgeRepo(pool), + settings_repo=SettingsRepo(pool), + user_repo=UserRepo(pool), + ) + reader = services.user_repo.create(username="reader", password_hash="h", role="user") + owner = services.user_repo.create(username="owner", password_hash="h", role="user") + allowed = services.knowledge_repo.create_base(owner_user_id=reader, name="Allowed") + hidden = services.knowledge_repo.create_base(owner_user_id=owner, name="Hidden") + allowed_doc = services.knowledge_repo.create_document( + kb_id=allowed.id, + filename="allowed.md", + content_type="text/markdown", + byte_size=1, + status="ready", + ) + hidden_doc = services.knowledge_repo.create_document( + kb_id=hidden.id, + filename="hidden.md", + content_type="text/markdown", + byte_size=1, + status="ready", + ) + KnowledgeIndex(allowed.id).replace_doc_chunks(allowed_doc.id, ["allowed fact"], [[1.0, 0.0]]) + KnowledgeIndex(hidden.id).replace_doc_chunks(hidden_doc.id, ["secret fact"], [[1.0, 0.0]]) + services.settings_repo.set("knowledge_embedding_model", "test-model") + monkeypatch.setattr(retrieve_module, "assert_knowledge_usable", lambda *_args: None) + monkeypatch.setattr( + retrieve_module, "embed_knowledge_texts", lambda _services, _texts: [[1.0, 0.0]] + ) + + context = asyncio.run( + retrieve_module.retrieve_context( + services, + user_id=reader, + is_admin=False, + query="What facts are available?", + knowledge_base_ids=[allowed.id, hidden.id], + ) + ) + + assert "allowed fact" in context + assert "allowed.md" in context + assert "secret fact" not in context + assert "hidden.md" not in context + + +def test_retrieve_context_skips_empty_non_text_turn() -> None: + context = asyncio.run( + retrieve_module.retrieve_context( + SimpleNamespace(), + user_id=1, + is_admin=False, + query=None, # type: ignore[arg-type] + knowledge_base_ids=["kb-1"], + ) + ) + + assert context == "" diff --git a/tests/unit/knowledge/test_service_acl.py b/tests/unit/knowledge/test_service_acl.py new file mode 100644 index 00000000..f220fa6c --- /dev/null +++ b/tests/unit/knowledge/test_service_acl.py @@ -0,0 +1,189 @@ +"""Unit tests for knowledge-base access control and document orchestration.""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from octop.infra.db.migrate import run_migrations +from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.knowledge import KnowledgeRepo +from octop.infra.db.repos.settings import SettingsRepo +from octop.infra.db.repos.users import UserRepo +from octop.infra.knowledge import service as service_module +from octop.infra.knowledge.service import MAX_DOCS_PER_KB, KnowledgeService +from octop.infra.utils.paths import PathLayout + + +@pytest.fixture +def service(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> KnowledgeService: + monkeypatch.setenv("OCTOP_HOME", str(tmp_path / "home")) + pool = SqlitePool(tmp_path / "octop.db") + run_migrations(pool) + services = SimpleNamespace( + knowledge_repo=KnowledgeRepo(pool), + settings_repo=SettingsRepo(pool), + user_repo=UserRepo(pool), + paths=PathLayout.from_env(), + ) + monkeypatch.setattr(service_module, "assert_knowledge_usable", lambda *_args: None) + return KnowledgeService(services) + + +def test_create_base_allows_shared_with_default_open(service: KnowledgeService) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + + kb = service.create_base( + owner_user_id=owner, + name="Docs", + shared=True, + default_open=True, + ) + assert kb.shared is True + assert kb.default_open is True + + +def test_update_base_allows_enabling_both_shared_and_default_open( + service: KnowledgeService, +) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + kb = service.create_base(owner_user_id=owner, name="Docs", shared=False, default_open=True) + + updated = service.update_base(kb.id, actor_user_id=owner, shared=True) + assert updated.shared is True + assert updated.default_open is True + + +def test_shared_reader_cannot_upload(service: KnowledgeService) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + viewer = users.create(username="viewer", password_hash="h", role="user") + kb = service._services.knowledge_repo.create_base(owner_user_id=owner, name="Docs", shared=True) + + with pytest.raises(PermissionError, match="write"): + service.upload_document( + kb.id, + actor_user_id=viewer, + filename="blocked.md", + content_type="text/markdown", + content=b"# blocked", + ) + + assert service.get_readable_base(kb.id, actor_user_id=viewer).id == kb.id + doc = service.upload_document( + kb.id, + actor_user_id=owner, + filename="allowed.md", + content_type="text/markdown", + content=b"# allowed", + ) + assert doc.filename == "allowed.md" + assert service._services.knowledge_repo.count_documents(kb.id) == 1 + assert service._services.knowledge_repo.get_base(kb.id).doc_count == 1 + + service.delete_document(kb.id, doc.id, actor_user_id=owner) + assert service._services.knowledge_repo.count_documents(kb.id) == 0 + assert service._services.knowledge_repo.get_base(kb.id).doc_count == 0 + + +def test_shared_reader_can_preview_document_text(service: KnowledgeService) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + viewer = users.create(username="viewer", password_hash="h", role="user") + kb = service._services.knowledge_repo.create_base(owner_user_id=owner, name="Docs", shared=True) + doc = service.upload_document( + kb.id, + actor_user_id=owner, + filename="notes.md", + content_type="text/markdown", + content=b"# Hello preview\n\nBody text.", + ) + + preview = service.preview_document(kb.id, doc.id, actor_user_id=viewer, is_admin=False) + + assert preview["id"] == doc.id + assert preview["filename"] == "notes.md" + assert "Hello preview" in preview["text"] + assert "Body text." in preview["text"] + + +def test_create_base_enforces_owner_limit(service: KnowledgeService) -> None: + from octop.infra.knowledge.service import MAX_BASES_PER_OWNER + + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + for ordinal in range(MAX_BASES_PER_OWNER): + service.create_base(owner_user_id=owner, name=f"kb-{ordinal}") + + with pytest.raises(ValueError, match="knowledge bases"): + service.create_base(owner_user_id=owner, name="one-too-many") + + +def test_upload_enforces_document_byte_limit(service: KnowledgeService) -> None: + from octop.infra.knowledge.service import MAX_DOCUMENT_BYTES + + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + kb = service.create_base(owner_user_id=owner, name="Docs") + + with pytest.raises(ValueError, match="document size"): + service.upload_document( + kb.id, + actor_user_id=owner, + filename="huge.md", + content_type="text/markdown", + content=b"x" * (MAX_DOCUMENT_BYTES + 1), + ) + + +def test_delete_document_works_when_feature_disabled(service: KnowledgeService) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + kb = service.create_base(owner_user_id=owner, name="Docs") + doc = service.upload_document( + kb.id, + actor_user_id=owner, + filename="notes.md", + content_type="text/markdown", + content=b"# hi", + ) + + import octop.infra.knowledge.service as mod + + def deny(*_args: object, **_kwargs: object) -> None: + raise RuntimeError("knowledge feature is disabled") + + original = mod.assert_knowledge_usable + mod.assert_knowledge_usable = deny # type: ignore[assignment] + try: + service.delete_document(kb.id, doc.id, actor_user_id=owner) + finally: + mod.assert_knowledge_usable = original + + assert service._services.knowledge_repo.get_document(doc.id) is None + + +def test_upload_enforces_document_limit(service: KnowledgeService) -> None: + users = service._services.user_repo + owner = users.create(username="owner", password_hash="h", role="user") + kb = service._services.knowledge_repo.create_base(owner_user_id=owner, name="Docs") + for ordinal in range(MAX_DOCS_PER_KB): + service._services.knowledge_repo.create_document( + kb_id=kb.id, + filename=f"{ordinal}.md", + content_type="text/markdown", + byte_size=1, + ) + + with pytest.raises(ValueError, match="100"): + service.upload_document( + kb.id, + actor_user_id=owner, + filename="one-too-many.md", + content_type="text/markdown", + content=b"x", + ) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 4318d007..cb70b158 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -23,9 +23,42 @@ def test_defaults_when_missing(tmp_path: Path): assert cfg.database.sqlite_path == "octop.db" assert cfg.database.is_sqlite assert cfg.database_in_file is False + assert cfg.backup.auto_enabled is False + assert cfg.backup.schedule == "cron:0 4 * * *" + assert cfg.backup.retention_count == 7 assert cfg_path.exists() # file written with defaults written = json.loads(cfg_path.read_text(encoding="utf-8")) assert "password" not in written.get("database", {}) + assert written.get("backup", {}).get("auto_enabled") is False + + +def test_loads_backup_section(tmp_path: Path): + cfg_path = tmp_path / "config.json" + cfg_path.write_text( + json.dumps( + { + "backup": { + "auto_enabled": True, + "schedule": "interval:7200", + "retention_count": 3, + } + } + ) + ) + cfg = load_config(cfg_path) + assert cfg.backup.auto_enabled is True + assert cfg.backup.schedule == "interval:7200" + assert cfg.backup.retention_count == 3 + + +def test_backup_env_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("OCTOP_BACKUP_AUTO_ENABLED", "true") + monkeypatch.setenv("OCTOP_BACKUP_SCHEDULE", "cron:30 5 * * *") + monkeypatch.setenv("OCTOP_BACKUP_RETENTION_COUNT", "5") + cfg = load_config(tmp_path / "config.json") + assert cfg.backup.auto_enabled is True + assert cfg.backup.schedule == "cron:30 5 * * *" + assert cfg.backup.retention_count == 5 def test_loads_existing(tmp_path: Path): diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index 7a9951c4..1456df1c 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -55,6 +55,11 @@ def test_backups_dir(tmp_path: Path) -> None: assert p.ensure_backups_dir().is_dir() +def test_knowledge_dir(tmp_path: Path) -> None: + p = PathLayout(tmp_path / ".octop") + assert p.knowledge_dir == tmp_path / ".octop" / "knowledge" + + def test_path_layout_from_env_defaults_to_dot_octop(monkeypatch) -> None: monkeypatch.delenv("OCTOP_HOME", raising=False) assert PathLayout.from_env().root == Path.home() / ".octop" diff --git a/tests/unit/test_provider_probe.py b/tests/unit/test_provider_probe.py index 28c9d632..7ca1e43c 100644 --- a/tests/unit/test_provider_probe.py +++ b/tests/unit/test_provider_probe.py @@ -3,9 +3,14 @@ from __future__ import annotations from types import SimpleNamespace -from unittest.mock import patch +from typing import Any +from unittest.mock import AsyncMock, patch + +import httpx +import pytest from octop.infra.agents.providers.probe import build_probe_chat_model as _build_chat_model +from octop.infra.agents.providers.probe import probe_provider_row def test_build_chat_model_includes_provider_id_and_model_name() -> None: @@ -26,3 +31,136 @@ def test_build_chat_model_includes_provider_id_and_model_name() -> None: assert provider.name == "HAI" assert model.id == "MiniMax-M2.7" assert model.name == "MiniMax-M2.7" + + +def _embedding_row(**overrides: Any) -> SimpleNamespace: + models = overrides.pop( + "models", + [{"id": "text-embedding-3-small", "name": "embed", "embedding": True}], + ) + data = { + "name": "openai", + "kind": "openai", + "base_url": "https://api.example.com/v1/", + "api_key": "sk-test", + "extra_json": None, + "get_models": lambda: models, + } + data.update(overrides) + return SimpleNamespace(**data) + + +def _mock_async_client(*, post: AsyncMock) -> AsyncMock: + mock_client = AsyncMock() + mock_client.post = post + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + return mock_client + + +@pytest.mark.asyncio +async def test_embedding_probe_posts_embeddings_endpoint() -> None: + request = httpx.Request("POST", "https://api.example.com/v1/embeddings") + response = httpx.Response(200, json={"data": [{"embedding": [0.1, 0.2]}]}, request=request) + post = AsyncMock(return_value=response) + with ( + patch( + "octop.infra.agents.providers.probe.httpx.AsyncClient", + return_value=_mock_async_client(post=post), + ), + patch("octop.infra.agents.providers.probe.build_probe_chat_model") as chat, + ): + result = await probe_provider_row(_embedding_row(), model_id="text-embedding-3-small") + + assert result["ok"] is True + assert isinstance(result["latency_ms"], int) + chat.assert_not_called() + args, kwargs = post.await_args + assert args[0] == "https://api.example.com/v1/embeddings" + assert kwargs["headers"]["Authorization"] == "Bearer sk-test" + assert kwargs["json"] == {"model": "text-embedding-3-small", "input": ["ping"]} + + +@pytest.mark.asyncio +async def test_embedding_probe_honors_explicit_flag_without_saved_model() -> None: + request = httpx.Request("POST", "https://api.openai.com/v1/embeddings") + response = httpx.Response(200, json={"data": [{"embedding": [1.0]}]}, request=request) + post = AsyncMock(return_value=response) + row = SimpleNamespace( + name="draft", + kind="openai", + base_url=None, + api_key="sk-test", + extra_json=None, + get_models=lambda: [{"id": "embed-1", "name": "embed-1"}], + ) + with patch( + "octop.infra.agents.providers.probe.httpx.AsyncClient", + return_value=_mock_async_client(post=post), + ): + result = await probe_provider_row(row, model_id="embed-1", embedding=True) + + assert result["ok"] is True + assert post.await_args.args[0] == "https://api.openai.com/v1/embeddings" + + +@pytest.mark.asyncio +async def test_embedding_probe_reports_http_error() -> None: + request = httpx.Request("POST", "https://api.example.com/v1/embeddings") + response = httpx.Response(401, json={"error": {"message": "bad key"}}, request=request) + post = AsyncMock(return_value=response) + with patch( + "octop.infra.agents.providers.probe.httpx.AsyncClient", + return_value=_mock_async_client(post=post), + ): + result = await probe_provider_row(_embedding_row()) + + assert result["ok"] is False + assert "401" in result["error"] + assert "POST https://api.example.com/v1/embeddings" in result["error"] + + +@pytest.mark.asyncio +async def test_embedding_probe_reports_empty_404_with_url() -> None: + request = httpx.Request("POST", "https://api.model.haihub.cn/v1/embeddings") + response = httpx.Response(404, text="", request=request) + post = AsyncMock(return_value=response) + row = _embedding_row( + name="HAI", + base_url="https://api.model.haihub.cn/v1", + models=[{"id": "bge-large-zh-v1.5", "name": "bge", "embedding": True}], + ) + with patch( + "octop.infra.agents.providers.probe.httpx.AsyncClient", + return_value=_mock_async_client(post=post), + ): + result = await probe_provider_row(row, model_id="bge-large-zh-v1.5") + + assert result["ok"] is False + assert result["error"] == "HTTP 404 POST https://api.model.haihub.cn/v1/embeddings" + + +@pytest.mark.asyncio +async def test_chat_probe_skips_embeddings_endpoint() -> None: + row = SimpleNamespace( + name="HAI", + kind="openai", + base_url="https://api.example.com/v1", + api_key="sk-test", + extra_json=None, + get_models=lambda: [{"id": "gpt-4o-mini", "name": "gpt-4o-mini"}], + ) + fake = AsyncMock() + fake.ainvoke = AsyncMock(return_value=SimpleNamespace(content="pong")) + with ( + patch( + "octop.infra.agents.providers.probe.build_probe_chat_model", + return_value=fake, + ), + patch("octop.infra.agents.providers.probe.httpx.AsyncClient") as client, + ): + result = await probe_provider_row(row, model_id="gpt-4o-mini") + + assert result["ok"] is True + client.assert_not_called() + fake.ainvoke.assert_awaited_once() diff --git a/tests/unit/test_shared.py b/tests/unit/test_shared.py index fb19a0eb..335eddc7 100644 --- a/tests/unit/test_shared.py +++ b/tests/unit/test_shared.py @@ -7,6 +7,7 @@ from octop.config import OctopConfig from octop.infra.db.migrate import run_migrations from octop.infra.db.pool import SqlitePool +from octop.infra.db.repos.sso import SsoRepo from octop.infra.db.services import SharedServices, build_shared_services from octop.infra.utils.paths import PathLayout @@ -24,3 +25,4 @@ def test_build_shared_services(tmp_path: Path): assert services.config is cfg # all repos resolved assert services.user_repo.count() == 0 + assert isinstance(services.sso_repo, SsoRepo) diff --git a/tests/unit/test_user_manager.py b/tests/unit/test_user_manager.py index 49bd7205..7b94cbf8 100644 --- a/tests/unit/test_user_manager.py +++ b/tests/unit/test_user_manager.py @@ -2,6 +2,7 @@ from __future__ import annotations +import sqlite3 from pathlib import Path import pytest @@ -16,6 +17,14 @@ from octop.infra.utils.paths import PathLayout +def _insert_sso_provider(manager: UserManager) -> int: + with manager._services.db.transaction() as conn: + cursor = conn.execute( + "INSERT INTO sso_providers(enabled, created_at, updated_at) VALUES (1, 0, 0)" + ) + return int(cursor.lastrowid) + + @pytest.fixture async def manager(tmp_path: Path) -> UserManager: paths = PathLayout(tmp_path / ".octop") @@ -54,6 +63,123 @@ async def test_authenticate_wrong_password(manager: UserManager): assert await manager.authenticate("a", "bad") is None +async def test_authenticate_rejects_null_password_hash(manager: UserManager): + manager._services.user_repo.create(username="sso_user", password_hash=None, role="user") + assert await manager.authenticate("sso_user", "any") is None + + +async def test_change_password_rejects_null_password_hash(manager: UserManager): + manager._services.user_repo.create(username="sso_user", password_hash=None, role="user") + with pytest.raises(OctopError) as ei: + await manager.change_password("sso_user", "any", "NewPass12") + assert ei.value.code is ErrorCode.AUTH_FAILED + + +async def test_sso_create_then_updates_same_subject(manager: UserManager): + provider_id = _insert_sso_provider(manager) + user = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"preferred_username": "alice", "email": "Alice@Example.com", "name": "Alice"}, + ) + + assert user.role is Role.USER + row = manager.get_row(user.id) + assert row.password_hash is None + assert row.email == "alice@example.com" + assert row.sso_provider_id == provider_id + assert row.sso_subject == "sub-1" + + updated = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"email": "alice-2@example.com", "name": "Alice 2"}, + ) + + assert updated.id == user.id + row = manager.get_row(user.id) + assert row.display_name == "Alice 2" + assert row.email == "alice-2@example.com" + + +async def test_sso_create_recovers_when_another_worker_wins_identity_race( + manager: UserManager, monkeypatch: pytest.MonkeyPatch +): + provider_id = _insert_sso_provider(manager) + repo = manager._services.user_repo + create = repo.create + + def create_after_race(*args: object, **kwargs: object) -> int: + create(*args, **kwargs) + raise sqlite3.IntegrityError( + "UNIQUE constraint failed: users.sso_provider_id, users.sso_subject" + ) + + monkeypatch.setattr(repo, "create", create_after_race) + + user = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-race", + claims={"preferred_username": "alice", "name": "Alice"}, + ) + + assert user.username == "alice" + assert repo.get_by_sso(provider_id, "sub-race").id == user.id + + +async def test_sso_username_allocation_sanitizes_and_suffixes_conflicts(manager: UserManager): + provider_id = _insert_sso_provider(manager) + first = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"preferred_username": "alice smith!"}, + ) + second = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-2", + claims={"preferred_username": "alice smith!"}, + ) + + assert first.username == "alicesmith" + assert second.username == "alicesmith_2" + + +async def test_sso_email_conflict_does_not_overwrite_existing_email(manager: UserManager): + provider_id = _insert_sso_provider(manager) + manager._services.user_repo.create( + username="local", + password_hash="not-used", + role="user", + email="taken@example.com", + ) + + user = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"preferred_username": "sso-user", "email": "taken@example.com"}, + ) + + assert manager.get_row(user.id).email is None + + +async def test_sso_disabled_user_is_rejected(manager: UserManager): + provider_id = _insert_sso_provider(manager) + user = await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"preferred_username": "alice"}, + ) + await manager.disable(user.username) + + with pytest.raises(OctopError) as ei: + await manager.resolve_or_create_sso_user( + provider_id=provider_id, + subject="sub-1", + claims={"name": "Alice"}, + ) + assert ei.value.code is ErrorCode.USER_DISABLED + + async def test_authenticate_locks_after_max_failures(manager: UserManager): await manager.create(username="a", password="TestPass12", role=Role.USER) max_attempts = manager._login_max_attempts diff --git a/tests/unit/users/test_permissions.py b/tests/unit/users/test_permissions.py new file mode 100644 index 00000000..347cc5d5 --- /dev/null +++ b/tests/unit/users/test_permissions.py @@ -0,0 +1,87 @@ +"""Unit tests for the module permission catalog.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pytest + +from octop.infra.users.permissions import ( + ALL_PERMISSION_KEYS, + BASELINE_PERMISSIONS, + PERMISSIONS, + effective_permissions, + user_has_permission, + validate_permission_keys, +) + + +@dataclass +class _FakeUser: + is_admin: bool + permissions: list[str] | None = None + + +def test_catalog_has_no_empty_keys_and_no_deferred_dead_keys() -> None: + assert PERMISSIONS + for key in PERMISSIONS: + assert key == PERMISSIONS[key].key + assert PERMISSIONS[key].label_zh and PERMISSIONS[key].label_en + assert "acp" not in PERMISSIONS + + +def test_user_has_permission_admin_bypass() -> None: + admin = _FakeUser(is_admin=True, permissions=[]) + for key in ALL_PERMISSION_KEYS: + assert user_has_permission(admin, key) is True + + +def test_user_has_permission_normal_hit_and_miss() -> None: + u = _FakeUser(is_admin=False, permissions=["browser", "users"]) + assert user_has_permission(u, "browser") is True + assert user_has_permission(u, "providers") is False + assert user_has_permission(u, "unknown_key") is False + + +def test_baseline_is_settings_group() -> None: + assert { + key for key, p in PERMISSIONS.items() if p.category == "settings" + } == BASELINE_PERMISSIONS + assert BASELINE_PERMISSIONS + assert all(PERMISSIONS[k].category == "settings" for k in BASELINE_PERMISSIONS) + + +def test_categories_match_nav_groups() -> None: + allowed = {"settings", "control", "admin"} + for p in PERMISSIONS.values(): + assert p.category in allowed + assert {k for k, p in PERMISSIONS.items() if p.category == "settings"} == { + "channels", + "connectors", + "skill_packages", + "knowledge_bases", + } + assert {k for k, p in PERMISSIONS.items() if p.category == "control"} == { + "terminal", + "browser", + "desktop", + } + assert PERMISSIONS["envs"].page == "advanced" + assert PERMISSIONS["knowledge_settings"].page == "advanced" + assert PERMISSIONS["knowledge_settings"].category == "admin" + assert "knowledge_settings" not in BASELINE_PERMISSIONS + assert PERMISSIONS["users"].page == "users" + assert PERMISSIONS["plugins"].page == "plugins" + assert not PERMISSIONS["plugins"].extra_tabs + assert "agents" not in PERMISSIONS + + +def test_validate_permission_keys_rejects_unknown() -> None: + assert validate_permission_keys(["browser", "users"]) == ["browser", "users"] + with pytest.raises(ValueError): + validate_permission_keys(["browser", "not_a_real_key"]) + + +def test_effective_permissions_admin_gets_catalog() -> None: + admin = _FakeUser(is_admin=True, permissions=[]) + assert effective_permissions(admin) == sorted(ALL_PERMISSION_KEYS) diff --git a/tests/unit/utils/test_runtime_packages.py b/tests/unit/utils/test_runtime_packages.py new file mode 100644 index 00000000..0b2337c5 --- /dev/null +++ b/tests/unit/utils/test_runtime_packages.py @@ -0,0 +1,125 @@ +"""Tests for runtime optional Python package installation.""" + +from __future__ import annotations + +import subprocess +import sys + +import pytest + +from octop.infra.utils import runtime_packages as mod + + +def test_find_uv_binary_from_path(monkeypatch) -> None: + monkeypatch.setenv("OCTOP_UV_BIN", "") + monkeypatch.setattr(mod.shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None) + assert mod.find_uv_binary() == "/usr/bin/uv" + + +def test_find_uv_binary_from_env_override(monkeypatch, tmp_path) -> None: + uv = tmp_path / "custom-uv" + uv.write_text("", encoding="utf-8") + monkeypatch.setenv("OCTOP_UV_BIN", str(uv)) + monkeypatch.setattr(mod.shutil, "which", lambda _name: None) + assert mod.find_uv_binary() == str(uv) + + +def test_build_install_commands_prefers_uv(monkeypatch) -> None: + monkeypatch.setattr(mod, "find_uv_binary", lambda: "/usr/bin/uv") + cmds = mod.build_install_commands(["pkg>=1"]) + assert cmds[0] == [ + "/usr/bin/uv", + "pip", + "install", + "--python", + sys.executable, + "pkg>=1", + ] + assert cmds[1][:4] == [sys.executable, "-m", "pip", "install"] + + +def test_install_packages_noop_when_satisfied() -> None: + assert mod.install_packages(["pkg"], is_satisfied=lambda: True) == "ready" + + +def test_install_packages_uses_uv_then_pip(monkeypatch) -> None: + calls: list[list[str]] = [] + state = {"ready": False} + + class FakeProc: + def __init__(self, code: int, stderr: str = "") -> None: + self.returncode = code + self.stderr = stderr + self.stdout = "" + + def fake_run(cmd, **_kwargs): # type: ignore[no-untyped-def] + calls.append(list(cmd)) + if cmd[0].endswith("uv"): + state["ready"] = True + return FakeProc(0) + return FakeProc(1, "should not reach pip") + + monkeypatch.setattr(mod, "find_uv_binary", lambda: "/usr/bin/uv") + monkeypatch.setattr(subprocess, "run", fake_run) + assert mod.install_packages(["pkg>=1"], is_satisfied=lambda: state["ready"]) == "installed" + assert calls[0][0] == "/usr/bin/uv" + + +def test_install_packages_bootstraps_pip_on_missing_module(monkeypatch) -> None: + calls: list[list[str]] = [] + state = {"pip": False, "ready": False} + + class FakeProc: + def __init__(self, code: int, stderr: str = "") -> None: + self.returncode = code + self.stderr = stderr + self.stdout = "" + + def fake_run(cmd, **_kwargs): # type: ignore[no-untyped-def] + calls.append(list(cmd)) + if cmd[:3] == [sys.executable, "-m", "ensurepip"]: + state["pip"] = True + return FakeProc(0) + if cmd[:3] == [sys.executable, "-m", "pip"]: + if not state["pip"]: + return FakeProc(1, "No module named pip") + state["ready"] = True + return FakeProc(0) + return FakeProc(1, "unexpected") + + monkeypatch.setattr(mod, "find_uv_binary", lambda: None) + monkeypatch.setattr(mod, "pip_importable", lambda: state["pip"]) + monkeypatch.setattr(subprocess, "run", fake_run) + assert mod.install_packages(["pkg>=1"], is_satisfied=lambda: state["ready"]) == "installed" + assert any(cmd[:3] == [sys.executable, "-m", "ensurepip"] for cmd in calls) + + +def test_install_packages_extra_fallback(monkeypatch) -> None: + calls: list[list[str]] = [] + + class FakeProc: + returncode = 1 + stderr = "network error" + stdout = "" + + monkeypatch.setattr(mod, "find_uv_binary", lambda: None) + monkeypatch.setattr(subprocess, "run", lambda cmd, **_kw: calls.append(list(cmd)) or FakeProc()) + spec = mod.PackageInstallSpec(packages=("fastembed>=0.4",), extra_fallback="local-embedding") + with pytest.raises(RuntimeError, match="Could not install optional Python components"): + mod.install_packages(spec, is_satisfied=lambda: False) + assert any("octop[local-embedding]" in cmd for cmd in calls) + + +def test_find_uv_binary_uses_custom_search_paths(monkeypatch, tmp_path) -> None: + uv = tmp_path / "uv" + uv.write_text("", encoding="utf-8") + monkeypatch.setenv("OCTOP_UV_BIN", "") + monkeypatch.setattr(mod.shutil, "which", lambda _name: None) + monkeypatch.setattr(mod, "_uv_search_paths", lambda: (uv,)) + assert mod.find_uv_binary() == str(uv) + + +def test_install_failed_message_hides_shell_commands() -> None: + msg = mod._install_failed_message("No module named pip") + assert "pip install" not in msg + assert "uv sync" not in msg diff --git a/uv.lock b/uv.lock index 439045c8..8c17e950 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 1 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15'", @@ -19,9 +19,9 @@ dependencies = [ { name = "pyrfc3339" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/5e/df38c186bcb5c2fc4827fa373a5c93a55cc0d82842af670e23c1e61c7867/acme-5.6.0.tar.gz", hash = "sha256:23f706ef1c437fc743b2f90704b97f263a56d83198b3af1c25c7dd42c4b463e4", size = 90686, upload-time = "2026-05-11T17:06:21.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/5e/df38c186bcb5c2fc4827fa373a5c93a55cc0d82842af670e23c1e61c7867/acme-5.6.0.tar.gz", hash = "sha256:23f706ef1c437fc743b2f90704b97f263a56d83198b3af1c25c7dd42c4b463e4", size = 90686 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/47/5eb27f6f065170380bd4bd782177a2f1f67e721d6b3fa7de18c492f52917/acme-5.6.0-py3-none-any.whl", hash = "sha256:fbf7ee8880485f2339942a2eff9a87deb79b74a8dcac4a572c08e7e640f046cd", size = 95023, upload-time = "2026-05-11T17:06:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/ba/47/5eb27f6f065170380bd4bd782177a2f1f67e721d6b3fa7de18c492f52917/acme-5.6.0-py3-none-any.whl", hash = "sha256:fbf7ee8880485f2339942a2eff9a87deb79b74a8dcac4a572c08e7e640f046cd", size = 95023 }, ] [[package]] @@ -31,9 +31,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/93/396d02c91b387b2678f23081869ca47bd495fc6c78789022c683180cf5f1/agent_client_protocol-0.11.0.tar.gz", hash = "sha256:8920a3b27bdcc852fd7c73066f47ab53257dbfbe57b505e20a40c69f80a5391c", size = 87402, upload-time = "2026-07-05T17:07:56.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/93/396d02c91b387b2678f23081869ca47bd495fc6c78789022c683180cf5f1/agent_client_protocol-0.11.0.tar.gz", hash = "sha256:8920a3b27bdcc852fd7c73066f47ab53257dbfbe57b505e20a40c69f80a5391c", size = 87402 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/48/a1367dded7765f5489f9840389949d5aeac53a73aeb1b4e6c0ab61e1617a/agent_client_protocol-0.11.0-py3-none-any.whl", hash = "sha256:2d8570cd4911f8af9fbab4808f7b05f09204a756f346c7409ecdcae3f37cdb2f", size = 68089, upload-time = "2026-07-05T17:07:55.518Z" }, + { url = "https://files.pythonhosted.org/packages/a0/48/a1367dded7765f5489f9840389949d5aeac53a73aeb1b4e6c0ab61e1617a/agent_client_protocol-0.11.0-py3-none-any.whl", hash = "sha256:2d8570cd4911f8af9fbab4808f7b05f09204a756f346c7409ecdcae3f37cdb2f", size = 68089 }, ] [[package]] @@ -44,9 +44,9 @@ dependencies = [ { name = "aiobotocore", extra = ["boto3"] }, { name = "aiofiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913 }, ] [[package]] @@ -62,9 +62,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039 }, ] [package.optional-dependencies] @@ -76,18 +76,18 @@ boto3 = [ name = "aiofiles" version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668 }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062 }, ] [[package]] @@ -104,99 +104,99 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, - { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, - { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, - { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, - { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, - { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402 }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310 }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448 }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854 }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884 }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034 }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054 }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278 }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795 }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504 }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806 }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707 }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121 }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580 }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771 }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873 }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073 }, + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882 }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270 }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841 }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088 }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564 }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998 }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918 }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657 }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907 }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565 }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416 }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881 }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572 }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137 }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953 }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479 }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077 }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688 }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094 }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662 }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748 }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723 }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531 }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718 }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918 }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014 }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398 }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018 }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462 }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824 }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898 }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114 }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541 }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776 }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329 }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293 }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756 }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052 }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888 }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679 }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021 }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574 }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773 }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001 }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809 }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320 }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077 }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476 }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347 }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465 }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423 }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906 }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095 }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922 }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035 }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512 }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571 }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159 }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409 }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166 }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255 }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640 }, ] [[package]] name = "aioitertools" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182 }, ] [[package]] @@ -207,18 +207,18 @@ dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, ] [[package]] name = "aiosqlite" version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405 }, ] [[package]] @@ -229,7 +229,7 @@ dependencies = [ { name = "cryptography" }, { name = "jmespath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555 } [[package]] name = "aliyun-python-sdk-kms" @@ -238,27 +238,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aliyun-python-sdk-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495 }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] @@ -275,9 +275,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/2a/f856135e5b055bf8b6f34133b313810dfe1cb848e5ac5ea843e196daefdd/anthropic-0.113.0.tar.gz", hash = "sha256:1830e866430ebd351c4f277d20e4c9b0aa9ad71f6569a23772ae88b33e0abaf8", size = 939444, upload-time = "2026-06-29T14:57:22.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/2a/f856135e5b055bf8b6f34133b313810dfe1cb848e5ac5ea843e196daefdd/anthropic-0.113.0.tar.gz", hash = "sha256:1830e866430ebd351c4f277d20e4c9b0aa9ad71f6569a23772ae88b33e0abaf8", size = 939444 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/4d/ea001c0993b8f48153c5eb74eecf5a3ed120df07fe894757351e8308bbe6/anthropic-0.113.0-py3-none-any.whl", hash = "sha256:9a52ed7a4982e916fa878d1ed3eaec2dcdf98699a39d8fc45b54b3c50f1e7426", size = 937995, upload-time = "2026-06-29T14:57:23.721Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/ea001c0993b8f48153c5eb74eecf5a3ed120df07fe894757351e8308bbe6/anthropic-0.113.0-py3-none-any.whl", hash = "sha256:9a52ed7a4982e916fa878d1ed3eaec2dcdf98699a39d8fc45b54b3c50f1e7426", size = 937995 }, ] [[package]] @@ -288,9 +288,9 @@ dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875 }, ] [[package]] @@ -300,9 +300,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzlocal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/6b/eeff360196bb20b312c9e762a820fd1b2c6d809466c755ef57863478e454/apscheduler-3.11.3.tar.gz", hash = "sha256:cd2fcc9330039a81a5893472ad49facf23a6d5604cbe1d918c835c6de7834d5a", size = 110312, upload-time = "2026-06-28T19:39:22.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/6b/eeff360196bb20b312c9e762a820fd1b2c6d809466c755ef57863478e454/apscheduler-3.11.3.tar.gz", hash = "sha256:cd2fcc9330039a81a5893472ad49facf23a6d5604cbe1d918c835c6de7834d5a", size = 110312 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024, upload-time = "2026-06-28T19:39:20.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024 }, ] [[package]] @@ -312,9 +312,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706 } 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" }, + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657 }, ] [[package]] @@ -324,142 +324,142 @@ 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180 }, + { 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 }, + { 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 }, ] [[package]] name = "ast-serialize" version = "0.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, - { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, - { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, - { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, - { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, - { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, - { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, - { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, - { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520 }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779 }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750 }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942 }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517 }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081 }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678 }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603 }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332 }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979 }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002 }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231 }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668 }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075 }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347 }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380 }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879 }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529 }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560 }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172 }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072 }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488 }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702 }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182 }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410 }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587 }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171 }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668 }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311 }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931 }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181 }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 }, ] [[package]] name = "audioop-lts" version = "0.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, - { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, - { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, - { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, - { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, - { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, - { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, - { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, - { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, - { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, - { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, - { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, - { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, - { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, - { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, - { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, - { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, - { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, - { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, - { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, - { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, - { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, - { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, - { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455 }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997 }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844 }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056 }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892 }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660 }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143 }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313 }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044 }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766 }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640 }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052 }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185 }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503 }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173 }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096 }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748 }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329 }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407 }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811 }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470 }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878 }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867 }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001 }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046 }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788 }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472 }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279 }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568 }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603 }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104 }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754 }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332 }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396 }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811 }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483 }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885 }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899 }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998 }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046 }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843 }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490 }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297 }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331 }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697 }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206 }, ] [[package]] name = "backoff" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, ] [[package]] @@ -470,18 +470,18 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924 }, ] [[package]] name = "bidict" version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, ] [[package]] @@ -493,9 +493,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321 }, ] [[package]] @@ -507,18 +507,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973 }, ] [[package]] name = "bracex" version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162, upload-time = "2026-06-30T00:43:35.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738, upload-time = "2026-06-30T00:43:34.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738 }, ] [[package]] @@ -530,18 +530,18 @@ dependencies = [ { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018 }, ] [[package]] name = "certifi" version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289 }, ] [[package]] @@ -551,127 +551,127 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, ] [[package]] name = "charset-normalizer" version = "3.4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328 }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061 }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031 }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239 }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589 }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733 }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652 }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229 }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552 }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806 }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316 }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274 }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468 }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460 }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330 }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828 }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627 }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008 }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303 }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282 }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595 }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986 }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711 }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036 }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998 }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056 }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537 }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176 }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723 }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085 }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819 }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915 }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234 }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042 }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706 }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727 }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882 }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860 }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564 }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276 }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238 }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189 }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352 }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024 }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869 }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541 }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634 }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384 }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133 }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257 }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851 }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393 }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609 }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014 }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979 }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238 }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110 }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824 }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103 }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194 }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168 }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018 }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958 }, ] [[package]] @@ -681,18 +681,18 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000 } 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" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 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" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] @@ -706,85 +706,85 @@ dependencies = [ { name = "six" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/c8/5cc0d0bd4b4bd9006976408bf390927fe51138658235c512a76799932de0/cos_python_sdk_v5-1.9.44.tar.gz", hash = "sha256:2ab403f3b64efdbb9a1984ad3e4381bd7f0755d1064148aeb25269bd757b49ab", size = 103461, upload-time = "2026-05-29T03:50:19.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/c8/5cc0d0bd4b4bd9006976408bf390927fe51138658235c512a76799932de0/cos_python_sdk_v5-1.9.44.tar.gz", hash = "sha256:2ab403f3b64efdbb9a1984ad3e4381bd7f0755d1064148aeb25269bd757b49ab", size = 103461 } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/ed/4eabca16f3f9049ac0ee5bd17ef42709a6ede659e81e97f74e77a1aae30b/cos_python_sdk_v5-1.9.44-py3-none-any.whl", hash = "sha256:584a97ecd5e9b15b04a1e511e6dd0ea4eca00ee87fe50aa9a3c91ce716149bcb", size = 99127, upload-time = "2026-05-29T03:50:17.35Z" }, + { url = "https://files.pythonhosted.org/packages/65/ed/4eabca16f3f9049ac0ee5bd17ef42709a6ede659e81e97f74e77a1aae30b/cos_python_sdk_v5-1.9.44-py3-none-any.whl", hash = "sha256:584a97ecd5e9b15b04a1e511e6dd0ea4eca00ee87fe50aa9a3c91ce716149bcb", size = 99127 }, ] [[package]] name = "coverage" version = "7.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672, upload-time = "2026-06-22T23:08:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035, upload-time = "2026-06-22T23:08:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540, upload-time = "2026-06-22T23:08:30.226Z" }, - { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274, upload-time = "2026-06-22T23:08:31.876Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389, upload-time = "2026-06-22T23:08:33.843Z" }, - { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648, upload-time = "2026-06-22T23:08:35.481Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949, upload-time = "2026-06-22T23:08:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310, upload-time = "2026-06-22T23:08:39.174Z" }, - { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453, upload-time = "2026-06-22T23:08:40.84Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522, upload-time = "2026-06-22T23:08:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023, upload-time = "2026-06-22T23:08:44.243Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893, upload-time = "2026-06-22T23:08:46.113Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734, upload-time = "2026-06-22T23:08:47.858Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266, upload-time = "2026-06-22T23:08:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655, upload-time = "2026-06-22T23:08:51.766Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, - { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, - { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, - { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, - { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, - { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, - { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, - { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, - { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, - { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, - { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, - { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, - { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, - { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, - { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, - { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, - { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672 }, + { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035 }, + { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540 }, + { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274 }, + { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389 }, + { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648 }, + { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949 }, + { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310 }, + { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453 }, + { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522 }, + { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023 }, + { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734 }, + { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266 }, + { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655 }, + { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699 }, + { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068 }, + { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060 }, + { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657 }, + { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892 }, + { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026 }, + { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285 }, + { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023 }, + { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989 }, + { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144 }, + { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808 }, + { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579 }, + { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741 }, + { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283 }, + { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678 }, + { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741 }, + { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068 }, + { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117 }, + { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622 }, + { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968 }, + { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284 }, + { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143 }, + { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976 }, + { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220 }, + { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756 }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413 }, + { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946 }, + { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436 }, + { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861 }, + { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474 }, + { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738 }, + { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101 }, + { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225 }, + { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643 }, + { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762 }, + { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208 }, + { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096 }, + { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433 }, + { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547 }, + { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859 }, + { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250 }, + { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082 }, + { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400 }, + { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469 }, ] [[package]] name = "crcmod" version = "1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670 } [[package]] name = "cryptography" @@ -793,47 +793,47 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100 }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978 }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422 }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503 }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779 }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683 }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874 }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283 }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844 }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290 }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612 }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804 }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026 }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892 }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835 }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239 }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593 }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961 }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145 }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719 }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209 }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285 }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441 }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869 }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948 }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153 }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947 }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429 }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968 }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758 }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863 }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983 }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173 }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298 }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338 }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650 }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820 }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968 }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547 }, ] [[package]] @@ -848,9 +848,9 @@ dependencies = [ { name = "langsmith" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364, upload-time = "2026-06-25T17:26:52.775Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/49/af7219b3c13520fee047bb807cfaefba17f8e4584c551d946773589a4f08/deepagents-0.6.12-py3-none-any.whl", hash = "sha256:28b8fa0119ca0a689e3e18e288c4634e4046062acfc87a1cb34289d3af3a1c88", size = 236120, upload-time = "2026-06-25T17:26:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/98/49/af7219b3c13520fee047bb807cfaefba17f8e4584c551d946773589a4f08/deepagents-0.6.12-py3-none-any.whl", hash = "sha256:28b8fa0119ca0a689e3e18e288c4634e4046062acfc87a1cb34289d3af3a1c88", size = 236120 }, ] [[package]] @@ -863,9 +863,9 @@ dependencies = [ { name = "psycopg", extra = ["binary", "pool"] }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/07/f999afc8d259384a942592661510a8fca9dc522d2711b7779596306cd516/deepagents_backends-0.2.0.tar.gz", hash = "sha256:b1522f0da651822c319304f30980f04ca885f79ace85aaeb9df84f723da5a821", size = 15497, upload-time = "2026-04-17T05:48:57.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/07/f999afc8d259384a942592661510a8fca9dc522d2711b7779596306cd516/deepagents_backends-0.2.0.tar.gz", hash = "sha256:b1522f0da651822c319304f30980f04ca885f79ace85aaeb9df84f723da5a821", size = 15497 } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/cb/bdf7adb4534ed3dad932a81825ef2b9e090fb96e1d5f18b3548131c918d9/deepagents_backends-0.2.0-py3-none-any.whl", hash = "sha256:0aff9b6971e38c887201095b408cdc7b5e223a4ed64d755c4108109c96938d3e", size = 13023, upload-time = "2026-04-17T05:48:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/69/cb/bdf7adb4534ed3dad932a81825ef2b9e090fb96e1d5f18b3548131c918d9/deepagents_backends-0.2.0-py3-none-any.whl", hash = "sha256:0aff9b6971e38c887201095b408cdc7b5e223a4ed64d755c4108109c96938d3e", size = 13023 }, ] [[package]] @@ -878,7 +878,7 @@ dependencies = [ { name = "websockets" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/44/102dede3f371277598df6aa9725b82e3add068c729333c7a5dbc12764579/dingtalk_stream-0.24.3-py3-none-any.whl", hash = "sha256:2160403656985962878bf60cdf5adf41619f21067348e06f07a7c7eebf5943ad", size = 27813, upload-time = "2025-10-24T09:36:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/4c/44/102dede3f371277598df6aa9725b82e3add068c729333c7a5dbc12764579/dingtalk_stream-0.24.3-py3-none-any.whl", hash = "sha256:2160403656985962878bf60cdf5adf41619f21067348e06f07a7c7eebf5943ad", size = 27813 }, ] [[package]] @@ -889,18 +889,18 @@ dependencies = [ { name = "aiohttp" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/57/9a2d9abdabdc9db8ef28ce0cf4129669e1c8717ba28d607b5ba357c4de3b/discord_py-2.7.1.tar.gz", hash = "sha256:24d5e6a45535152e4b98148a9dd6b550d25dc2c9fb41b6d670319411641249da", size = 1106326, upload-time = "2026-03-03T18:40:46.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/57/9a2d9abdabdc9db8ef28ce0cf4129669e1c8717ba28d607b5ba357c4de3b/discord_py-2.7.1.tar.gz", hash = "sha256:24d5e6a45535152e4b98148a9dd6b550d25dc2c9fb41b6d670319411641249da", size = 1106326 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/a7/17208c3b3f92319e7fad259f1c6d5a5baf8fd0654c54846ced329f83c3eb/discord_py-2.7.1-py3-none-any.whl", hash = "sha256:849dca2c63b171146f3a7f3f8acc04248098e9e6203412ce3cf2745f284f7439", size = 1227550, upload-time = "2026-03-03T18:40:44.492Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a7/17208c3b3f92319e7fad259f1c6d5a5baf8fd0654c54846ced329f83c3eb/discord_py-2.7.1-py3-none-any.whl", hash = "sha256:849dca2c63b171146f3a7f3f8acc04248098e9e6203412ce3cf2745f284f7439", size = 1227550 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } 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" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] @@ -912,18 +912,18 @@ dependencies = [ { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/7f/731ff914b0255d3d065f45fd4e626d4b8c95dbcbaada049f337a6ac16410/docker-7.2.0.tar.gz", hash = "sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac", size = 118731, upload-time = "2026-07-09T14:53:46.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/7f/731ff914b0255d3d065f45fd4e626d4b8c95dbcbaada049f337a6ac16410/docker-7.2.0.tar.gz", hash = "sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac", size = 118731 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775, upload-time = "2026-07-09T14:53:45.224Z" }, + { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341 } 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" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484 }, ] [[package]] @@ -936,9 +936,9 @@ dependencies = [ { name = "tabulate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/60/afbf548b43c78355e03926c6b1fff7500303a2da4d84db9e1324119e21ae/edge_tts-7.2.8.tar.gz", hash = "sha256:fcf185a0d527a0d2d003f9d5841facc1d5e0e7b3b88d5df9c32990402c6b8cd0", size = 27875, upload-time = "2026-03-22T19:57:50.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/60/afbf548b43c78355e03926c6b1fff7500303a2da4d84db9e1324119e21ae/edge_tts-7.2.8.tar.gz", hash = "sha256:fcf185a0d527a0d2d003f9d5841facc1d5e0e7b3b88d5df9c32990402c6b8cd0", size = 27875 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/2b/a8cb687b92a2690d2ad171f0c2fd1c8f18690363cca7618bab2bbe4cdf2b/edge_tts-7.2.8-py3-none-any.whl", hash = "sha256:361fe48ce7ef613adbe30f664e3765dd71029c6cb57427279eff8ad6df2eb211", size = 31026, upload-time = "2026-03-22T19:57:49.672Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2b/a8cb687b92a2690d2ad171f0c2fd1c8f18690363cca7618bab2bbe4cdf2b/edge_tts-7.2.8-py3-none-any.whl", hash = "sha256:361fe48ce7ef613adbe30f664e3765dd71029c6cb57427279eff8ad6df2eb211", size = 31026 }, ] [[package]] @@ -950,13 +950,13 @@ dependencies = [ { name = "pycryptodome" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/d4/c9a2b33935c620678bd5acf5e32feda70ef09f384b24eb17a5719f72f5fe/esdk_obs_python-3.26.6.tar.gz", hash = "sha256:5014e76e85ffa9eda821169302e74868991867ac947da73bc28eb0e6dfba3ab1", size = 116430, upload-time = "2026-07-12T07:09:36.698Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/d4/c9a2b33935c620678bd5acf5e32feda70ef09f384b24eb17a5719f72f5fe/esdk_obs_python-3.26.6.tar.gz", hash = "sha256:5014e76e85ffa9eda821169302e74868991867ac947da73bc28eb0e6dfba3ab1", size = 116430 } [[package]] name = "evdev" version = "1.9.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723, upload-time = "2026-02-05T21:54:24.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723 } [[package]] name = "fastapi" @@ -969,107 +969,154 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/a9/9f8f7e00195c29836e9bf58bbbaf579e29878b8a67851efff93d9b6d4eb7/fastapi-0.138.2.tar.gz", hash = "sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8", size = 420423, upload-time = "2026-06-29T12:44:12.556Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/a9/9f8f7e00195c29836e9bf58bbbaf579e29878b8a67851efff93d9b6d4eb7/fastapi-0.138.2.tar.gz", hash = "sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8", size = 420423 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271 }, +] + +[[package]] +name = "fastembed" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "loguru" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "pillow" }, + { name = "py-rust-stemmers" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/25/58865e36b6e8a9a0d0ff905b5601aa30db97956327c0df42ec4ed6accc21/fastembed-0.8.0.tar.gz", hash = "sha256:75966edfa8b006ee78514c726bd7f6a50721dadc89305279052be9db72fd53e8", size = 75115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e8/26b7d78bb8972498c467ca34cb12ee2e60d26ba5eae6d8443189a1af37a5/fastembed-0.8.0-py3-none-any.whl", hash = "sha256:40bee672657574a1009e35ec50030a55f2b426842cb011845379817641bbbbd0", size = 116572 }, +] + +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830 }, ] [[package]] name = "filetype" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, ] [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583 }, ] [[package]] @@ -1083,9 +1130,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492, upload-time = "2026-06-03T14:52:17.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102 }, ] [[package]] @@ -1099,9 +1146,9 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/53/0cd38e3a29d72ce45e27feba2ce1cd8049d69af9c48cb14fb164f1be9133/google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a", size = 15060142, upload-time = "2026-06-25T14:32:42.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/53/0cd38e3a29d72ce45e27feba2ce1cd8049d69af9c48cb14fb164f1be9133/google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a", size = 15060142 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/92/0fc9e7a09eb240c31b879bd8d2e43f81ed1f86c4798b79ead4a083921ab3/google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e", size = 15644203, upload-time = "2026-06-25T14:32:39.963Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/0fc9e7a09eb240c31b879bd8d2e43f81ed1f86c4798b79ead4a083921ab3/google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e", size = 15644203 }, ] [[package]] @@ -1112,9 +1159,9 @@ dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349 }, ] [package.optional-dependencies] @@ -1130,9 +1177,9 @@ dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/f192c8bc7e41e0ebdbd95afcae4783417a34b6a6af62d22daf22c3fd38fc/google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35", size = 11161, upload-time = "2026-05-07T08:03:46.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/f192c8bc7e41e0ebdbd95afcae4783417a34b6a6af62d22daf22c3fd38fc/google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35", size = 11161 } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/be/954c35a62b9e31de66b0a43c225c9b6bb9e0f98d6b1dc110a2308e3644f5/google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91", size = 9529, upload-time = "2026-05-07T08:02:12.375Z" }, + { url = "https://files.pythonhosted.org/packages/97/be/954c35a62b9e31de66b0a43c225c9b6bb9e0f98d6b1dc110a2308e3644f5/google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91", size = 9529 }, ] [[package]] @@ -1151,9 +1198,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/fe/b796087493c3c55371aa58b9f264841ace5bfdf8c668cafa7afa33c44bec/google_genai-2.10.0.tar.gz", hash = "sha256:77912cd558cd7dfd5b75c25fd1c609e78d7954dde583331104022a46ea90f9ee", size = 600039, upload-time = "2026-06-24T01:33:18.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/fe/b796087493c3c55371aa58b9f264841ace5bfdf8c668cafa7afa33c44bec/google_genai-2.10.0.tar.gz", hash = "sha256:77912cd558cd7dfd5b75c25fd1c609e78d7954dde583331104022a46ea90f9ee", size = 600039 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/39/00bcfd94de255d24249401efff4f48d77bf6066b46447e519fa193c0c299/google_genai-2.10.0-py3-none-any.whl", hash = "sha256:d5350311567ae660c24cbc1752aee4b3d660f89c0106d2dcd2a69978c35afe1e", size = 957974, upload-time = "2026-06-24T01:33:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/39/00bcfd94de255d24249401efff4f48d77bf6066b46447e519fa193c0c299/google_genai-2.10.0-py3-none-any.whl", hash = "sha256:d5350311567ae660c24cbc1752aee4b3d660f89c0106d2dcd2a69978c35afe1e", size = 957974 }, ] [[package]] @@ -1163,85 +1210,85 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631 }, ] [[package]] name = "greenlet" version = "3.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, - { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, - { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" }, - { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, - { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, - { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, - { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" }, - { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, - { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657, upload-time = "2026-06-26T19:24:19.967Z" }, - { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, - { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497, upload-time = "2026-06-26T19:25:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, - { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, - { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, - { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, - { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582, upload-time = "2026-06-26T19:24:21.357Z" }, - { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037, upload-time = "2026-06-26T19:25:40.672Z" }, - { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, - { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261, upload-time = "2026-06-26T19:24:22.79Z" }, - { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322, upload-time = "2026-06-26T19:25:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, - { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, - { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, - { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, - { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706, upload-time = "2026-06-26T19:24:24.312Z" }, - { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803, upload-time = "2026-06-26T19:25:43.063Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, - { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685 }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761 }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044 }, + { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351 }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244 }, + { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395 }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210 }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627 }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882 }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211 }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609 }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435 }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787 }, + { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580 }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786 }, + { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933 }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316 }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850 }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141 }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202 }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096 }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304 }, + { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657 }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635 }, + { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497 }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252 }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824 }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754 }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549 }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389 }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382 }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401 }, + { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582 }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969 }, + { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037 }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822 }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983 }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626 }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860 }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747 }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419 }, + { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261 }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787 }, + { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322 }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305 }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631 }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027 }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085 }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221 }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221 }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226 }, + { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706 }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802 }, + { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803 }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157 }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159 }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007 }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } 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" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] @@ -1254,9 +1301,9 @@ dependencies = [ { name = "pydantic" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/33/440362a9bd055b21699eaa09699a017fc782df7e11841dc2ea46792603a5/harness_browser-0.7.4.tar.gz", hash = "sha256:f27c530cc7db1e356dcc45a4a0f7262907df30809b631753bf320560fa51b0e3", size = 314856, upload-time = "2026-07-13T11:21:32.284Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/33/440362a9bd055b21699eaa09699a017fc782df7e11841dc2ea46792603a5/harness_browser-0.7.4.tar.gz", hash = "sha256:f27c530cc7db1e356dcc45a4a0f7262907df30809b631753bf320560fa51b0e3", size = 314856 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/67/b603397707d8bbab842e1c86a67da559b0c1bf9c74a55b5a4ea27492ace4/harness_browser-0.7.4-py3-none-any.whl", hash = "sha256:d9ca1a7636a8509634d7b021e74cda409be4a28f27be6af47f134193d33f913a", size = 92072, upload-time = "2026-07-13T11:21:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/10/67/b603397707d8bbab842e1c86a67da559b0c1bf9c74a55b5a4ea27492ace4/harness_browser-0.7.4-py3-none-any.whl", hash = "sha256:d9ca1a7636a8509634d7b021e74cda409be4a28f27be6af47f134193d33f913a", size = 92072 }, ] [[package]] @@ -1279,18 +1326,42 @@ dependencies = [ { name = "websockets" }, { name = "wecom-aibot-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/b3/51b707a92829f01c9baeb725854f1f293ff4e3e5e9d7f492e24876ed2336/harness_gateway-0.9.2.tar.gz", hash = "sha256:b6219a098cce91e88f84b337eae6b349ffa4475d2f4d0afa492372f8c6633187", size = 493828, upload-time = "2026-08-10T14:56:59.782Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/b3/51b707a92829f01c9baeb725854f1f293ff4e3e5e9d7f492e24876ed2336/harness_gateway-0.9.2.tar.gz", hash = "sha256:b6219a098cce91e88f84b337eae6b349ffa4475d2f4d0afa492372f8c6633187", size = 493828 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/83/f3735c5609796126cef04f697ac26355f580853038f058938c8139bf14f1/harness_gateway-0.9.2-py3-none-any.whl", hash = "sha256:1fa7df9a4f26a20e2dffb622145c916c896f403289d60efb610d15afca6e3269", size = 137219, upload-time = "2026-08-10T14:56:57.241Z" }, + { url = "https://files.pythonhosted.org/packages/3a/83/f3735c5609796126cef04f697ac26355f580853038f058938c8139bf14f1/harness_gateway-0.9.2-py3-none-any.whl", hash = "sha256:1fa7df9a4f26a20e2dffb622145c916c896f403289d60efb610d15afca6e3269", size = 137219 }, ] [[package]] name = "harness-memory" version = "0.9.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/f1/338ceac50178d2ff6e690050fde2dc83de332012ab4a721a19e9647371bf/harness_memory-0.9.5.tar.gz", hash = "sha256:7950697b4209320195d03bcda39a640f09f7b97fc95f08acc74d5ee6756e75da", size = 912009, upload-time = "2026-08-02T06:00:31.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/f1/338ceac50178d2ff6e690050fde2dc83de332012ab4a721a19e9647371bf/harness_memory-0.9.5.tar.gz", hash = "sha256:7950697b4209320195d03bcda39a640f09f7b97fc95f08acc74d5ee6756e75da", size = 912009 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/31/cbaf29ff9b9c0ac28aa85d9999d2f32dd6895c586c8510d22b7c96a94d8c/harness_memory-0.9.5-py3-none-any.whl", hash = "sha256:e1aff4ca3a7405ee5a53fcf75c5cb4b80e3ea56193a09a969fc3423c03156994", size = 316204 }, +] + +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527 } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/31/cbaf29ff9b9c0ac28aa85d9999d2f32dd6895c586c8510d22b7c96a94d8c/harness_memory-0.9.5-py3-none-any.whl", hash = "sha256:e1aff4ca3a7405ee5a53fcf75c5cb4b80e3ea56193a09a969fc3423c03156994", size = 316204, upload-time = "2026-08-02T06:00:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584 }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381 }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982 }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445 }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099 }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712 }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906 }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221 }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729 }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287 }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663 }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538 }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937 }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128 }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359 }, ] [[package]] @@ -1301,9 +1372,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } 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" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -1313,45 +1384,45 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283 } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148 }, ] [[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" }, +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209 }, ] [[package]] @@ -1364,113 +1435,133 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 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" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, +] + +[[package]] +name = "huggingface-hub" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } 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" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } 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" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] [[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" }, +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473 }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905 }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853 }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160 }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578 }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154 }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523 }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366 }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, ] [[package]] name = "jmespath" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489 }, ] [[package]] @@ -1480,9 +1571,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500, upload-time = "2025-10-14T14:54:42.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211, upload-time = "2025-10-14T14:54:41.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211 }, ] [[package]] @@ -1492,18 +1583,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpointer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 }, ] [[package]] name = "jsonpointer" version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659 }, ] [[package]] @@ -1516,9 +1607,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 }, ] [[package]] @@ -1528,9 +1619,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, ] [[package]] @@ -1542,9 +1633,9 @@ dependencies = [ { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/a2/91a7197c604a3ce1b774b3c10dd114c3c745c6186a304fc2573b3f94d400/langchain-1.3.11.tar.gz", hash = "sha256:f3cf9cd4d2329b1a03eb8fd92b9d73e4e58a4d52570d67725fc77fbe0f104b32", size = 633374, upload-time = "2026-06-22T23:00:33.44Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a2/91a7197c604a3ce1b774b3c10dd114c3c745c6186a304fc2573b3f94d400/langchain-1.3.11.tar.gz", hash = "sha256:f3cf9cd4d2329b1a03eb8fd92b9d73e4e58a4d52570d67725fc77fbe0f104b32", size = 633374 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/a4/3a181967294f8876362cc4ba36840d50b8286fa23bb3f5e602b69eb3cb1e/langchain-1.3.11-py3-none-any.whl", hash = "sha256:7ae011f95a09b22feea1e8ae4e43f0b6164aebf4c61b8ad845b45f72ff3a90a2", size = 133639, upload-time = "2026-06-22T23:00:31.619Z" }, + { url = "https://files.pythonhosted.org/packages/df/a4/3a181967294f8876362cc4ba36840d50b8286fa23bb3f5e602b69eb3cb1e/langchain-1.3.11-py3-none-any.whl", hash = "sha256:7ae011f95a09b22feea1e8ae4e43f0b6164aebf4c61b8ad845b45f72ff3a90a2", size = 133639 }, ] [[package]] @@ -1556,9 +1647,9 @@ dependencies = [ { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366, upload-time = "2026-06-26T21:28:45.535Z" }, + { url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366 }, ] [[package]] @@ -1571,9 +1662,9 @@ dependencies = [ { name = "numpy" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/1d/bb306951b1c394b7a27effb8eb6c9ee65dd77fcc4be7c20f76e3299a9e1e/langchain_aws-1.1.0.tar.gz", hash = "sha256:1e2f8570328eae4907c3cf7e900dc68d8034ddc865d9dc96823c9f9d8cccb901", size = 393899, upload-time = "2025-11-24T14:35:24.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/1d/bb306951b1c394b7a27effb8eb6c9ee65dd77fcc4be7c20f76e3299a9e1e/langchain_aws-1.1.0.tar.gz", hash = "sha256:1e2f8570328eae4907c3cf7e900dc68d8034ddc865d9dc96823c9f9d8cccb901", size = 393899 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/33/91b8d2a7570657b371382b45054142c54165a51706990a5c1b4cc40c0e9a/langchain_aws-1.1.0-py3-none-any.whl", hash = "sha256:8ec074615b42839e035354063717374c32c63f5028ef5221ba073fd5f3ef5e37", size = 152432, upload-time = "2025-11-24T14:35:23.004Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/91b8d2a7570657b371382b45054142c54165a51706990a5c1b4cc40c0e9a/langchain_aws-1.1.0-py3-none-any.whl", hash = "sha256:8ec074615b42839e035354063717374c32c63f5028ef5221ba073fd5f3ef5e37", size = 152432 }, ] [[package]] @@ -1589,9 +1680,9 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/65/6b5e8a7ff2f2968652c88a67dcecb925b9d8f0a0ce9458c76cd5a0dbd138/langchain_classic-1.0.8.tar.gz", hash = "sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484", size = 10557071, upload-time = "2026-06-10T21:27:54.866Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/65/6b5e8a7ff2f2968652c88a67dcecb925b9d8f0a0ce9458c76cd5a0dbd138/langchain_classic-1.0.8.tar.gz", hash = "sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484", size = 10557071 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/9a/b8f5cb7490fdbf233088031fc69c9c747439d4097f67f196c1eb4869916d/langchain_classic-1.0.8-py3-none-any.whl", hash = "sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213", size = 1041536, upload-time = "2026-06-10T21:27:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/99/9a/b8f5cb7490fdbf233088031fc69c9c747439d4097f67f196c1eb4869916d/langchain_classic-1.0.8-py3-none-any.whl", hash = "sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213", size = 1041536 }, ] [[package]] @@ -1611,9 +1702,9 @@ dependencies = [ { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/0c/e3aca1f2b1c5b95f8b87cb2b6e81a6f20d538c07a128419dc01cef0617b6/langchain_community-0.4.2.tar.gz", hash = "sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e", size = 33268708, upload-time = "2026-05-22T19:42:59.374Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/0c/e3aca1f2b1c5b95f8b87cb2b6e81a6f20d538c07a128419dc01cef0617b6/langchain_community-0.4.2.tar.gz", hash = "sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e", size = 33268708 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/39/5d97e42a3e95dc2a6d71b2f902a3fae71786131e11d01bddb604accb0ebe/langchain_community-0.4.2-py3-none-any.whl", hash = "sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466", size = 2364406, upload-time = "2026-05-22T19:42:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/5d97e42a3e95dc2a6d71b2f902a3fae71786131e11d01bddb604accb0ebe/langchain_community-0.4.2-py3-none-any.whl", hash = "sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466", size = 2364406 }, ] [[package]] @@ -1631,9 +1722,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" }, + { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416 }, ] [[package]] @@ -1646,9 +1737,9 @@ dependencies = [ { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/08/68e4f90b273d1d8dc3a7a948e6535e60baac726adbb2a8341dcf17662f54/langchain_google_genai-4.2.6.tar.gz", hash = "sha256:653dc331e691ddd79784d9ff6a4082749e0f873394c6dc782c414eb4409850eb", size = 280074, upload-time = "2026-06-26T06:18:58.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/08/68e4f90b273d1d8dc3a7a948e6535e60baac726adbb2a8341dcf17662f54/langchain_google_genai-4.2.6.tar.gz", hash = "sha256:653dc331e691ddd79784d9ff6a4082749e0f873394c6dc782c414eb4409850eb", size = 280074 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/3c/9a6b43bced0238f95640555e14423fe4a8268e3f4f536f67e26e79e633d4/langchain_google_genai-4.2.6-py3-none-any.whl", hash = "sha256:c40db0c2d033a5fb6db8e2cc3fb6d49c5678b89b337a64da095fb73ec9f72021", size = 70457, upload-time = "2026-06-26T06:18:57.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/3c/9a6b43bced0238f95640555e14423fe4a8268e3f4f536f67e26e79e633d4/langchain_google_genai-4.2.6-py3-none-any.whl", hash = "sha256:c40db0c2d033a5fb6db8e2cc3fb6d49c5678b89b337a64da095fb73ec9f72021", size = 70457 }, ] [[package]] @@ -1660,9 +1751,9 @@ dependencies = [ { name = "mcp" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/1c/b179d8650d2349a342bc1fd1aab41b34154e79c7fc86fc42bdf0bb110d6f/langchain_mcp_adapters-0.3.0.tar.gz", hash = "sha256:fa6c9497015eb2807de5d0c341a36e1d2445cecbae1f4a24e922fc5b94f1a36c", size = 42774, upload-time = "2026-06-10T01:10:28.552Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/1c/b179d8650d2349a342bc1fd1aab41b34154e79c7fc86fc42bdf0bb110d6f/langchain_mcp_adapters-0.3.0.tar.gz", hash = "sha256:fa6c9497015eb2807de5d0c341a36e1d2445cecbae1f4a24e922fc5b94f1a36c", size = 42774 } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/89/b4869db84ce529de6c7548319197df50c24d0f8a7412f74f889e22324036/langchain_mcp_adapters-0.3.0-py3-none-any.whl", hash = "sha256:1af511a95e028d9546502e360f95698ae8b691dbc07981fc48170c2cb1ebd7a9", size = 25855, upload-time = "2026-06-10T01:10:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/b4869db84ce529de6c7548319197df50c24d0f8a7412f74f889e22324036/langchain_mcp_adapters-0.3.0-py3-none-any.whl", hash = "sha256:1af511a95e028d9546502e360f95698ae8b691dbc07981fc48170c2cb1ebd7a9", size = 25855 }, ] [[package]] @@ -1674,9 +1765,9 @@ dependencies = [ { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/f3/b38e052f943a75ba0e6762c0a50b6f1d6bcd52a9ce63386d8803c2ff506e/langchain_openai-1.3.3.tar.gz", hash = "sha256:143769bf943820b80db769e47ca8fd0aac08ed18714519333b044c4431e9aa67", size = 3256559, upload-time = "2026-06-22T22:54:05.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f3/b38e052f943a75ba0e6762c0a50b6f1d6bcd52a9ce63386d8803c2ff506e/langchain_openai-1.3.3.tar.gz", hash = "sha256:143769bf943820b80db769e47ca8fd0aac08ed18714519333b044c4431e9aa67", size = 3256559 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/4b/7520114de1ce36bb17cbc98d0fcda048a9f755bedaeb73b92137aeaaf1db/langchain_openai-1.3.3-py3-none-any.whl", hash = "sha256:e469659862c8aabba4f6653df973206e7be54f98cf2275c86be7f06b7abe20d7", size = 120437, upload-time = "2026-06-22T22:54:03.8Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4b/7520114de1ce36bb17cbc98d0fcda048a9f755bedaeb73b92137aeaaf1db/langchain_openai-1.3.3-py3-none-any.whl", hash = "sha256:e469659862c8aabba4f6653df973206e7be54f98cf2275c86be7f06b7abe20d7", size = 120437 }, ] [[package]] @@ -1686,9 +1777,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221 }, ] [[package]] @@ -1701,9 +1792,9 @@ dependencies = [ { name = "langchain-core" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/b309ef3062b189a82463dc93553804566e71aa393f9ba8954750793c1a6f/langchain_tavily-0.2.18.tar.gz", hash = "sha256:cd7859ae1a6ce79236580ef67072ff5fc43c7ded94e7eac38ff04209ca85a320", size = 25378, upload-time = "2026-04-16T15:23:24.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/b309ef3062b189a82463dc93553804566e71aa393f9ba8954750793c1a6f/langchain_tavily-0.2.18.tar.gz", hash = "sha256:cd7859ae1a6ce79236580ef67072ff5fc43c7ded94e7eac38ff04209ca85a320", size = 25378 } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/9c/0c043e4434b1823f0ac194f66036cbb0569275a99dcb890e0891ecd34fb2/langchain_tavily-0.2.18-py3-none-any.whl", hash = "sha256:dccf3ad1c50e2cb2a89bec11727555805c9df8abd42c1f3ad42ccad86e28aa44", size = 30814, upload-time = "2026-04-16T15:23:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/9c/0c043e4434b1823f0ac194f66036cbb0569275a99dcb890e0891ecd34fb2/langchain_tavily-0.2.18-py3-none-any.whl", hash = "sha256:dccf3ad1c50e2cb2a89bec11727555805c9df8abd42c1f3ad42ccad86e28aa44", size = 30814 }, ] [[package]] @@ -1713,9 +1804,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903 }, ] [[package]] @@ -1732,9 +1823,9 @@ dependencies = [ { name = "pydantic" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/40/752661e4376dcaa73bf839703296530d91807e90e219e1fc0d24c5460bc2/langfuse-4.14.0.tar.gz", hash = "sha256:31b875c8d09eee39c558584b9424bbd5ed014965c5944c4528a37947ae4b7787", size = 363802, upload-time = "2026-07-10T11:33:39.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/752661e4376dcaa73bf839703296530d91807e90e219e1fc0d24c5460bc2/langfuse-4.14.0.tar.gz", hash = "sha256:31b875c8d09eee39c558584b9424bbd5ed014965c5944c4528a37947ae4b7787", size = 363802 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c7/ca0f591e1184415ffcd920a4107f3d2638ab5af7ff7e498d99a9b8b2bb13/langfuse-4.14.0-py3-none-any.whl", hash = "sha256:632b74abfa14d9ad3dccbe3322bf7e11b0dce9be0de451b0d134db9bff246d44", size = 638356, upload-time = "2026-07-10T11:33:37.419Z" }, + { url = "https://files.pythonhosted.org/packages/76/c7/ca0f591e1184415ffcd920a4107f3d2638ab5af7ff7e498d99a9b8b2bb13/langfuse-4.14.0-py3-none-any.whl", hash = "sha256:632b74abfa14d9ad3dccbe3322bf7e11b0dce9be0de451b0d134db9bff246d44", size = 638356 }, ] [[package]] @@ -1749,9 +1840,9 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/11/c3961537ac7577125aabba68effc33ecc3dc0962e2d287d734dbd61caad7/langgraph-1.2.7.tar.gz", hash = "sha256:dcdf5b441bf8c7c7c154e603b302c9dbfbfd2d11e1b7ae7d93a5aba979dc87bd", size = 721385, upload-time = "2026-06-30T01:24:12.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/11/c3961537ac7577125aabba68effc33ecc3dc0962e2d287d734dbd61caad7/langgraph-1.2.7.tar.gz", hash = "sha256:dcdf5b441bf8c7c7c154e603b302c9dbfbfd2d11e1b7ae7d93a5aba979dc87bd", size = 721385 } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/5d/19de0c674cf2d8bc95b25cdb4c13ba3f4fbe0f35bf2224ef19cc46bbb4cd/langgraph-1.2.7-py3-none-any.whl", hash = "sha256:249349a5d6a32cb0aa2304a8fb6529bd0cf56db8780d21db9e086e5383668f89", size = 246930, upload-time = "2026-06-30T01:24:11.15Z" }, + { url = "https://files.pythonhosted.org/packages/16/5d/19de0c674cf2d8bc95b25cdb4c13ba3f4fbe0f35bf2224ef19cc46bbb4cd/langgraph-1.2.7-py3-none-any.whl", hash = "sha256:249349a5d6a32cb0aa2304a8fb6529bd0cf56db8780d21db9e086e5383668f89", size = 246930 }, ] [[package]] @@ -1762,9 +1853,9 @@ dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212 }, ] [[package]] @@ -1777,9 +1868,9 @@ dependencies = [ { name = "psycopg" }, { name = "psycopg-pool" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/51/5a2dc42e8b5d5942b933b5b7237eae5a4dbc92508a04727c263dd383ad8a/langgraph_checkpoint_postgres-3.1.0.tar.gz", hash = "sha256:02bff4ab63d9dae8eab3a9640fce1d479da8965c9fba7b0dc04cb1f7c56f0a55", size = 148473, upload-time = "2026-05-12T03:40:10.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/51/5a2dc42e8b5d5942b933b5b7237eae5a4dbc92508a04727c263dd383ad8a/langgraph_checkpoint_postgres-3.1.0.tar.gz", hash = "sha256:02bff4ab63d9dae8eab3a9640fce1d479da8965c9fba7b0dc04cb1f7c56f0a55", size = 148473 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/cd/eff9b82bc3b5f62d481b437099f44f3ef7b1d907f166fb4ee25e8f84a1e7/langgraph_checkpoint_postgres-3.1.0-py3-none-any.whl", hash = "sha256:814cce2ef35d792bf07b090a95eed004f1acac0724fe6605536b13f6d1e7032c", size = 48988, upload-time = "2026-05-12T03:40:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cd/eff9b82bc3b5f62d481b437099f44f3ef7b1d907f166fb4ee25e8f84a1e7/langgraph_checkpoint_postgres-3.1.0-py3-none-any.whl", hash = "sha256:814cce2ef35d792bf07b090a95eed004f1acac0724fe6605536b13f6d1e7032c", size = 48988 }, ] [[package]] @@ -1791,9 +1882,9 @@ dependencies = [ { name = "langgraph-checkpoint" }, { name = "sqlite-vec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/ea/83917c2369acf8a10a894d4247655fd063c07924ba5bc4e83c85d2eaeded/langgraph_checkpoint_sqlite-3.1.0.tar.gz", hash = "sha256:f926916ebc1b985d802cc9c820026036e84db9d910d62c97b57e4ba64f67d5ae", size = 147902, upload-time = "2026-05-12T03:34:52.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ea/83917c2369acf8a10a894d4247655fd063c07924ba5bc4e83c85d2eaeded/langgraph_checkpoint_sqlite-3.1.0.tar.gz", hash = "sha256:f926916ebc1b985d802cc9c820026036e84db9d910d62c97b57e4ba64f67d5ae", size = 147902 } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/07/b342811a16327900af2747c752ea19676172fcddf9b592cc384031076623/langgraph_checkpoint_sqlite-3.1.0-py3-none-any.whl", hash = "sha256:cc9b40df0076feae8a9ad42ae713621b148b00ac23adc09dc1dc66090a46e5ad", size = 38587, upload-time = "2026-05-12T03:34:51.231Z" }, + { url = "https://files.pythonhosted.org/packages/97/07/b342811a16327900af2747c752ea19676172fcddf9b592cc384031076623/langgraph_checkpoint_sqlite-3.1.0-py3-none-any.whl", hash = "sha256:cc9b40df0076feae8a9ad42ae713621b148b00ac23adc09dc1dc66090a46e5ad", size = 38587 }, ] [[package]] @@ -1804,9 +1895,9 @@ dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043 }, ] [[package]] @@ -1820,9 +1911,9 @@ dependencies = [ { name = "orjson" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521 }, ] [[package]] @@ -1845,9 +1936,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/b7/43f80940a758ceb48714ccc10c7acb9564be4f0d4786e8ed5d46deb0be39/langsmith-0.9.3.tar.gz", hash = "sha256:868a007b3ecda002914b21212a953c77fcb1d71a8a09a94562c3b57941d3df3a", size = 4577380, upload-time = "2026-06-26T15:56:30.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/b7/43f80940a758ceb48714ccc10c7acb9564be4f0d4786e8ed5d46deb0be39/langsmith-0.9.3.tar.gz", hash = "sha256:868a007b3ecda002914b21212a953c77fcb1d71a8a09a94562c3b57941d3df3a", size = 4577380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/fb/dc9f70b5581371fba7b01ba0704fe859bfcd12c4dad82b42ec12bd4160b1/langsmith-0.9.3-py3-none-any.whl", hash = "sha256:aac85686868a7e61d77858b880230e510583c61ea48ac0a197e6ceff64ffb22b", size = 619927, upload-time = "2026-06-26T15:56:28.301Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fb/dc9f70b5581371fba7b01ba0704fe859bfcd12c4dad82b42ec12bd4160b1/langsmith-0.9.3-py3-none-any.whl", hash = "sha256:aac85686868a7e61d77858b880230e510583c61ea48ac0a197e6ceff64ffb22b", size = 619927 }, ] [[package]] @@ -1862,147 +1953,160 @@ dependencies = [ { name = "websockets" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/45/83/67095719058122617d663eaad89e06c93bffec414b5aff45a76cb4d46ae5/lark_oapi-1.7.0-py3-none-any.whl", hash = "sha256:d38ca7be0f6b42e51d459c00cf0a8975de1e4e5752e8f59e1bbce5bda0735282", size = 7806351, upload-time = "2026-06-30T03:19:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/45/83/67095719058122617d663eaad89e06c93bffec414b5aff45a76cb4d46ae5/lark_oapi-1.7.0-py3-none-any.whl", hash = "sha256:d38ca7be0f6b42e51d459c00cf0a8975de1e4e5752e8f59e1bbce5bda0735282", size = 7806351 }, ] [[package]] name = "librt" version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147 }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614 }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538 }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623 }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082 }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105 }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268 }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348 }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294 }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608 }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879 }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831 }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470 }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119 }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565 }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395 }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383 }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010 }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433 }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595 }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255 }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847 }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920 }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898 }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812 }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448 }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345 }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131 }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024 }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221 }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174 }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216 }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921 }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850 }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237 }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261 }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965 }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151 }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850 }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138 }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976 }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927 }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698 }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162 }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494 }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858 }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318 }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115 }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918 }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562 }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327 }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, ] [[package]] name = "lxml" version = "6.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, - { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, - { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, - { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, - { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, - { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, - { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, - { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, - { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, - { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, - { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, - { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, - { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, - { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, - { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, - { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, - { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, - { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, - { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, - { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, - { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, - { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, - { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, - { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, - { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, - { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, - { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, - { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, - { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, - { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, - { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, - { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, - { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, - { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821 }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252 }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746 }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723 }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557 }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036 }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367 }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171 }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874 }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492 }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232 }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023 }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773 }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088 }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995 }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382 }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255 }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610 }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780 }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006 }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139 }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329 }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564 }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467 }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304 }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607 }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168 }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487 }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231 }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450 }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874 }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987 }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276 }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903 }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869 }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490 }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146 }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866 }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022 }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695 }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642 }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338 }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528 }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730 }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530 }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670 }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485 }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635 }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681 }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229 }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191 }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202 }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497 }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991 }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545 }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736 }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291 }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822 }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923 }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843 }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515 }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511 }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206 }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404 }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769 }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936 }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296 }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598 }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845 }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345 }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350 }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223 }, ] [[package]] @@ -2012,9 +2116,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 }, ] [[package]] @@ -2025,9 +2129,9 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724 }, ] [[package]] @@ -2050,126 +2154,208 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087 }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500 }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034 }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292 }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274 }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158 }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005 }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744 }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111 }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623 }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437 }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002 }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507 }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773 }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560 }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313 }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832 }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964 }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114 }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819 }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330 }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078 }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498 }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320 }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222 }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048 }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742 }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132 }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686 }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479 }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030 }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536 }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769 }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563 }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794 }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131 }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825 }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344 }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291 }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052 }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311 }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279 }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290 }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116 }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740 }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143 }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703 }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484 }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012 }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508 }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387 }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101 }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836 }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727 }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308 }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758 }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670 }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399 }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386 }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924 }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280 }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050 }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158 }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890 }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121 }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187 }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036 }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391 }, ] [[package]] name = "mss" version = "10.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/5d/eee782a6d674f562c946ae6a026f4c595ea2b7b031f290bf9fbf60da09b5/mss-10.2.0.tar.gz", hash = "sha256:ab271860775545e62f29d7b11f82f279ac1048f5bbdd26cfad84830208dbd393", size = 200317, upload-time = "2026-04-23T10:44:57.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/5d/eee782a6d674f562c946ae6a026f4c595ea2b7b031f290bf9fbf60da09b5/mss-10.2.0.tar.gz", hash = "sha256:ab271860775545e62f29d7b11f82f279ac1048f5bbdd26cfad84830208dbd393", size = 200317 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c3/313e14f245c79b4c05bd0f3a84a4813aa26fa10f8993aebd91d04c5fad3f/mss-10.2.0-py3-none-any.whl", hash = "sha256:e79f428899280e7e64e38365b5bfed683851ebea807eeaeadaf06eb8e0d67197", size = 67106, upload-time = "2026-04-23T10:44:56.266Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c3/313e14f245c79b4c05bd0f3a84a4813aa26fa10f8993aebd91d04c5fad3f/mss-10.2.0-py3-none-any.whl", hash = "sha256:e79f428899280e7e64e38365b5bfed683851ebea807eeaeadaf06eb8e0d67197", size = 67106 }, ] [[package]] name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893 }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456 }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872 }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018 }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883 }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413 }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404 }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456 }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322 }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955 }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254 }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059 }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588 }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642 }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377 }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887 }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053 }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307 }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174 }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116 }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524 }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368 }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952 }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317 }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140 }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277 }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291 }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156 }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742 }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664 }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490 }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695 }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884 }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122 }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175 }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460 }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930 }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582 }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596 }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492 }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899 }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970 }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060 }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888 }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554 }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341 }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391 }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422 }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109 }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573 }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190 }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486 }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219 }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132 }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420 }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510 }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786 }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483 }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403 }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315 }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528 }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784 }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980 }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602 }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930 }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074 }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471 }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401 }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143 }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507 }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358 }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884 }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878 }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542 }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403 }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889 }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982 }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415 }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337 }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788 }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842 }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237 }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008 }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542 }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719 }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319 }, ] [[package]] @@ -2183,102 +2369,102 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381 }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501 }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750 }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630 }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831 }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228 }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684 }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174 }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542 }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929 }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200 }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435 }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052 }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422 }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374 }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743 }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371 }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429 }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799 }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458 }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697 }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638 }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852 }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695 }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622 }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798 }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } 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" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] name = "numpy" version = "2.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, - { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, - { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, - { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574 }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250 }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516 }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863 }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977 }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469 }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531 }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940 }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764 }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966 }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488 }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419 }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832 }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143 }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749 }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716 }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440 }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305 }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008 }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885 }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674 }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256 }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063 }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652 }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944 }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266 }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720 }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835 }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684 }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103 }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177 }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739 }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709 }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810 }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189 }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039 }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306 }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955 }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252 }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298 }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623 }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674 }, ] [[package]] name = "octop" -version = "0.9.22" +version = "0.9.23" source = { editable = "." } dependencies = [ { name = "acme" }, @@ -2302,7 +2488,9 @@ dependencies = [ { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pyjwt" }, + { name = "pypdf" }, { name = "python-docx" }, + { name = "python-pptx" }, { name = "questionary" }, { name = "rich" }, { name = "scalar-fastapi" }, @@ -2326,8 +2514,13 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-testmon" }, { name = "ruff" }, ] +local-embedding = [ + { name = "fastembed" }, + { name = "huggingface-hub" }, +] [package.dev-dependencies] dev = [ @@ -2337,6 +2530,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-testmon" }, { name = "ruff" }, ] @@ -2351,10 +2545,12 @@ requires-dist = [ { name = "cryptography", specifier = ">=41" }, { name = "edge-tts", specifier = ">=6.1" }, { name = "fastapi", specifier = ">=0.110" }, + { name = "fastembed", marker = "extra == 'local-embedding'", specifier = ">=0.4" }, { name = "harness-browser", specifier = ">=0.7.4" }, { name = "harness-gateway", specifier = ">=0.9.2" }, { name = "harness-memory", specifier = ">=0.9.5" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" }, + { name = "huggingface-hub", marker = "extra == 'local-embedding'", specifier = ">=0.20" }, { name = "josepy", specifier = ">=2.2.0" }, { name = "langchain-core", specifier = ">=1.4.8" }, { name = "langgraph-checkpoint-postgres", specifier = ">=2.0" }, @@ -2369,10 +2565,13 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.6,<2.14" }, { name = "pyjwt", specifier = ">=2.8" }, { name = "pynput", marker = "extra == 'desktop'", specifier = ">=1.7" }, + { name = "pypdf", specifier = ">=5.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1" }, + { name = "pytest-testmon", marker = "extra == 'dev'", specifier = ">=2.0" }, { name = "python-docx", specifier = ">=1.2.0" }, + { name = "python-pptx", specifier = ">=1.0" }, { name = "questionary", specifier = ">=2.0" }, { name = "rich", specifier = ">=13.7" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, @@ -2381,7 +2580,7 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.27" }, { name = "websockets", specifier = ">=13.0" }, ] -provides-extras = ["dev", "browser", "desktop"] +provides-extras = ["dev", "browser", "desktop", "local-embedding"] [package.metadata.requires-dev] dev = [ @@ -2391,9 +2590,42 @@ dev = [ { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.23" }, { name = "pytest-cov", specifier = ">=4.1" }, + { name = "pytest-testmon", specifier = ">=2.0" }, { name = "ruff", specifier = ">=0.4" }, ] +[[package]] +name = "onnxruntime" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362 }, + { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628 }, + { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257 }, + { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036 }, + { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462 }, + { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759 }, + { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339 }, + { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329 }, + { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033 }, + { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175 }, + { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307 }, + { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954 }, + { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748 }, + { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950 }, + { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924 }, + { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738 }, + { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117 }, + { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518 }, + { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976 }, +] + [[package]] name = "openai" version = "2.44.0" @@ -2408,9 +2640,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216 }, ] [[package]] @@ -2420,9 +2652,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912 }, ] [[package]] @@ -2432,9 +2664,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197, upload-time = "2026-06-24T15:20:00.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048, upload-time = "2026-06-24T15:19:41.264Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048 }, ] [[package]] @@ -2450,9 +2682,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/92/0b9f56412483a8891d4843890294796c9df8ab42417bd9bad8035d840cb3/opentelemetry_exporter_otlp_proto_http-1.43.0.tar.gz", hash = "sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f", size = 25406, upload-time = "2026-06-24T15:20:01.515Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/92/0b9f56412483a8891d4843890294796c9df8ab42417bd9bad8035d840cb3/opentelemetry_exporter_otlp_proto_http-1.43.0.tar.gz", hash = "sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f", size = 25406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/20/b685ed7af2e17c29ffc8af56f1fa8bc2033258fc30fb0d2b722f49d13ba0/opentelemetry_exporter_otlp_proto_http-1.43.0-py3-none-any.whl", hash = "sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610", size = 21795, upload-time = "2026-06-24T15:19:43.164Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/b685ed7af2e17c29ffc8af56f1fa8bc2033258fc30fb0d2b722f49d13ba0/opentelemetry_exporter_otlp_proto_http-1.43.0-py3-none-any.whl", hash = "sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610", size = 21795 }, ] [[package]] @@ -2462,9 +2694,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481, upload-time = "2026-06-24T15:20:07.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489, upload-time = "2026-06-24T15:19:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489 }, ] [[package]] @@ -2476,9 +2708,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" }, + { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852 }, ] [[package]] @@ -2489,9 +2721,9 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713 }, ] [[package]] @@ -2514,9 +2746,9 @@ dependencies = [ { name = "mcp" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/7e/5c749a8b53debaaa175fdad66c81963daf9b6559deabd10bafd0d366155b/orcakit_harness_agent-0.9.20.tar.gz", hash = "sha256:dac5112226c633bf3a0e07193cde430a6d2901b6dae35b4514166e4da28322ce", size = 1144223, upload-time = "2026-08-10T14:53:41.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/7e/5c749a8b53debaaa175fdad66c81963daf9b6559deabd10bafd0d366155b/orcakit_harness_agent-0.9.20.tar.gz", hash = "sha256:dac5112226c633bf3a0e07193cde430a6d2901b6dae35b4514166e4da28322ce", size = 1144223 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/a5/1f0f1c3c700ba97cbd472a356d8bff4f58e21c2a2dc3987e57c586db3bf3/orcakit_harness_agent-0.9.20-py3-none-any.whl", hash = "sha256:41e9311222d118e8c26f066b71131bdd2d2ecf6e4572e0c62eaef8e5598294c4", size = 1364449, upload-time = "2026-08-10T14:53:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/df/a5/1f0f1c3c700ba97cbd472a356d8bff4f58e21c2a2dc3987e57c586db3bf3/orcakit_harness_agent-0.9.20-py3-none-any.whl", hash = "sha256:41e9311222d118e8c26f066b71131bdd2d2ecf6e4572e0c62eaef8e5598294c4", size = 1364449 }, ] [package.optional-dependencies] @@ -2542,92 +2774,92 @@ all = [ name = "orjson" version = "3.11.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, - { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, - { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, - { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, - { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, - { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, - { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, - { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, - { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, - { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, - { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, - { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, - { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, - { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, - { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, - { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, - { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, - { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, - { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515 }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409 }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106 }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864 }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213 }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994 }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744 }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014 }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509 }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127 }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025 }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943 }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606 }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736 }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458 }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368 }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070 }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892 }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217 }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980 }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738 }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033 }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492 }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087 }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031 }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915 }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613 }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086 }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696 }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465 }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364 }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063 }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356 }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592 }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491 }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309 }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030 }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482 }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178 }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089 }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921 }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638 }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078 }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687 }, ] [[package]] name = "ormsgpack" version = "1.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, - { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, - { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, - { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, - { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, - { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, - { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, - { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, - { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, - { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, - { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, - { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, - { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618 }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186 }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738 }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569 }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166 }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498 }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518 }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462 }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559 }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661 }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194 }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778 }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592 }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164 }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539 }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459 }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577 }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717 }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183 }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814 }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634 }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139 }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578 }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539 }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493 }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579 }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721 }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170 }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816 }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232 }, ] [[package]] @@ -2642,104 +2874,104 @@ dependencies = [ { name = "requests" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845 } [[package]] name = "packaging" version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 }, ] [[package]] name = "paho-mqtt" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180 } 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" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328 }, ] [[package]] name = "pillow" version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969 }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323 }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838 }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830 }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383 }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934 }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684 }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137 }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267 }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684 }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487 }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433 }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889 }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109 }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736 }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129 }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562 }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439 }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691 }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185 }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, ] [[package]] @@ -2751,23 +2983,23 @@ dependencies = [ { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/44/ee/31e4e0db36588b817a10b299a0285082545fde7d36543c2abe498bb3d61a/playwright-1.61.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ff138c3a604f69911e9d42fd036e55c2a171e5616edf04c1e7f60a2a285540b0", size = 43421877, upload-time = "2026-06-29T10:32:48.428Z" }, - { url = "https://files.pythonhosted.org/packages/42/35/71395dd3ecc798965be4a3ef8c443217d4abca168e7cb34536304f9489e6/playwright-1.61.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:009588c2a7e499bc5a8b425b61fa65490968bbda9cd69e0cf2cff10f8304659a", size = 42205016, upload-time = "2026-06-29T10:32:52.104Z" }, - { url = "https://files.pythonhosted.org/packages/f4/44/323164cf5cd1647bdefce76ffce27651aadb959d089b48f53ea40918276e/playwright-1.61.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9f7de4536088d12037c13a52b7ea34b59270b78926bb56935070597ffac6b1af", size = 43421884, upload-time = "2026-06-29T10:32:55.773Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/a35bf179e4ba2522c1893635094a64e407572547bd61528820fc0abc87fe/playwright-1.61.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:54f3b39f6eab832e33458c1dd7da0b5682aedab3b09ae731b5c59fa12fd2024e", size = 47421381, upload-time = "2026-06-29T10:32:59.903Z" }, - { url = "https://files.pythonhosted.org/packages/b7/eb/e3f922348ec17c315f98c463f72faa1181a1c3de0bfe31a8d2edf6561723/playwright-1.61.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93454322ade8c11d5d6c211bfd91bdfb9ffb4810e3e026371bcbc4bec1b7ee4c", size = 47120545, upload-time = "2026-06-29T10:33:03.574Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a6/5be4e52b40a9c0c8a073e7c5b0785c05cf5a9ea8f8a7b5b260e32d970342/playwright-1.61.0-py3-none-win32.whl", hash = "sha256:372d55a6f1248fa1dd47599686980cb8fb5bbe6fcda59eab793eb657c11d8a9b", size = 37844841, upload-time = "2026-06-29T10:33:07.361Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fd/2b78036e5fbe9d5f5645bbe08a1eac7160c51243c0093963edbcf67c35d9/playwright-1.61.0-py3-none-win_amd64.whl", hash = "sha256:35c6cc4589a5d00964a59d7b3e59641e0aac0c02f15479a7af77d20f6bc79597", size = 37844846, upload-time = "2026-06-29T10:33:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/1b0f3c4ee4eb0514bc805b5c2f9a223e5b6de4f11a926f5235d51d0fc81b/playwright-1.61.0-py3-none-win_arm64.whl", hash = "sha256:e9fcbffcf557a8620fdedd92491eb59a32d18e23d6f3b4f6214b952be324fe51", size = 33955127, upload-time = "2026-06-29T10:33:14.008Z" }, + { url = "https://files.pythonhosted.org/packages/44/ee/31e4e0db36588b817a10b299a0285082545fde7d36543c2abe498bb3d61a/playwright-1.61.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ff138c3a604f69911e9d42fd036e55c2a171e5616edf04c1e7f60a2a285540b0", size = 43421877 }, + { url = "https://files.pythonhosted.org/packages/42/35/71395dd3ecc798965be4a3ef8c443217d4abca168e7cb34536304f9489e6/playwright-1.61.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:009588c2a7e499bc5a8b425b61fa65490968bbda9cd69e0cf2cff10f8304659a", size = 42205016 }, + { url = "https://files.pythonhosted.org/packages/f4/44/323164cf5cd1647bdefce76ffce27651aadb959d089b48f53ea40918276e/playwright-1.61.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9f7de4536088d12037c13a52b7ea34b59270b78926bb56935070597ffac6b1af", size = 43421884 }, + { url = "https://files.pythonhosted.org/packages/ab/f8/a35bf179e4ba2522c1893635094a64e407572547bd61528820fc0abc87fe/playwright-1.61.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:54f3b39f6eab832e33458c1dd7da0b5682aedab3b09ae731b5c59fa12fd2024e", size = 47421381 }, + { url = "https://files.pythonhosted.org/packages/b7/eb/e3f922348ec17c315f98c463f72faa1181a1c3de0bfe31a8d2edf6561723/playwright-1.61.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93454322ade8c11d5d6c211bfd91bdfb9ffb4810e3e026371bcbc4bec1b7ee4c", size = 47120545 }, + { url = "https://files.pythonhosted.org/packages/c2/a6/5be4e52b40a9c0c8a073e7c5b0785c05cf5a9ea8f8a7b5b260e32d970342/playwright-1.61.0-py3-none-win32.whl", hash = "sha256:372d55a6f1248fa1dd47599686980cb8fb5bbe6fcda59eab793eb657c11d8a9b", size = 37844841 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/2b78036e5fbe9d5f5645bbe08a1eac7160c51243c0093963edbcf67c35d9/playwright-1.61.0-py3-none-win_amd64.whl", hash = "sha256:35c6cc4589a5d00964a59d7b3e59641e0aac0c02f15479a7af77d20f6bc79597", size = 37844846 }, + { url = "https://files.pythonhosted.org/packages/27/0d/1b0f3c4ee4eb0514bc805b5c2f9a223e5b6de4f11a926f5235d51d0fc81b/playwright-1.61.0-py3-none-win_arm64.whl", hash = "sha256:e9fcbffcf557a8620fdedd92491eb59a32d18e23d6f3b4f6214b952be324fe51", size = 33955127 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } 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" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] @@ -2777,103 +3009,103 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198 } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 }, ] [[package]] name = "propcache" version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, - { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, - { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, - { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, - { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, - { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887 }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654 }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190 }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995 }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422 }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342 }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639 }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588 }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029 }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532 }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592 }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788 }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514 }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018 }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322 }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172 }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457 }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835 }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545 }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886 }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261 }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184 }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534 }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500 }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994 }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884 }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464 }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588 }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667 }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463 }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621 }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649 }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636 }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872 }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257 }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696 }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378 }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616 }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773 }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664 }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643 }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595 }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711 }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247 }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102 }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964 }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546 }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330 }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521 }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662 }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928 }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650 }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912 }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300 }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208 }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633 }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724 }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069 }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099 }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391 }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626 }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781 }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570 }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436 }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373 }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554 }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395 }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653 }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914 }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567 }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542 }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845 }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985 }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999 }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779 }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796 }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023 }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448 }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329 }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172 }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813 }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140 }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036 }, ] [[package]] @@ -2883,52 +3115,52 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/44/767757fd2cdd4a60d7e4440d9f7b491d6131103d313638d2c03e06c268fb/proto_plus-1.28.1.tar.gz", hash = "sha256:832e68e7fe064cf90ab153b6e5eb935b27891bb89aaeb68b115e9b702f6cb168", size = 57166, upload-time = "2026-07-08T17:04:02.367Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/44/767757fd2cdd4a60d7e4440d9f7b491d6131103d313638d2c03e06c268fb/proto_plus-1.28.1.tar.gz", hash = "sha256:832e68e7fe064cf90ab153b6e5eb935b27891bb89aaeb68b115e9b702f6cb168", size = 57166 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl", hash = "sha256:6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed", size = 50408, upload-time = "2026-07-08T17:03:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl", hash = "sha256:6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed", size = 50408 }, ] [[package]] name = "protobuf" version = "7.35.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226 }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945 }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996 }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595 }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082 }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476 }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062 }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893 }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589 }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664 }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087 }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383 }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210 }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228 }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284 }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090 }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859 }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560 }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997 }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972 }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266 }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737 }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617 }, ] [[package]] @@ -2939,9 +3171,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, upload-time = "2026-05-01T23:31:55.179Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799 } 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" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001 }, ] [package.optional-dependencies] @@ -2957,39 +3189,39 @@ 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" }, + { 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 }, ] [[package]] @@ -2999,18 +3231,56 @@ 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023 }, +] + +[[package]] +name = "py-rust-stemmers" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/c1/9763f9fb1cd73f9c317a83feeed6e0d4af320c6bbddab47b4a94f3a47d0c/py_rust_stemmers-0.1.8.tar.gz", hash = "sha256:6b0f6f48bc54d607aed802de872fcd5a71bae969a6760976dc78ce55e8eaf3da", size = 9732 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/6a/39080bc8f4a441a35378c0faeeb834fb27974997f40d51342574e70f9662/py_rust_stemmers-0.1.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6a9a4b8733d0b307bd0879ab7e321aa8a0bfd054a75a5cb23c647df5ca7d17c3", size = 290230 }, + { url = "https://files.pythonhosted.org/packages/73/15/ae60b9010924adac465f418822d9c514690aba6846edd67b6e2b5c227745/py_rust_stemmers-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51d0042d2a92ef0f7048bfc06b6c2a02306af31ea47f09d24b34e4b7e63c4e80", size = 275449 }, + { url = "https://files.pythonhosted.org/packages/ec/7c/94be8b932179823d66e0d2be03a94706132a7d16a640d5e5710de1cb1b8f/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d3d34094b9b6078a8ea6fe1c7044e5fd32f14e76c94818c5008f49ae075f08", size = 316676 }, + { url = "https://files.pythonhosted.org/packages/f3/a4/8bd5c9f31207136830457d819e3f98bb21c54c0cdc40d6f1845ce4efdf7c/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40c86be90cee4a709ad84fde4db7f11ca44d65630a56b77ec86fe84c23adfc09", size = 319458 }, + { url = "https://files.pythonhosted.org/packages/f9/95/95da2b353b164a3a2b8a1c799866a58060693be4f1dc21065663dc67dc17/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515884bcfb47b10335146648f276930d0c1201ae5e8b7b400fb46d8ea05c0ec2", size = 323541 }, + { url = "https://files.pythonhosted.org/packages/3e/ce/f34403b68808519dfa3220e1d94a40f26d5025f27e28893e2388ab9cfde5/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa42f5f8feb694aaaa869eedf477fcaf66f67a192cd64d94302d06920c33864a", size = 323873 }, + { url = "https://files.pythonhosted.org/packages/57/01/fb8527f6474d576975415405c985a97260e0403829e062103d334230b7d2/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e86ad68fe297a6652f0f0390625ea81858b6f27862fd4c5ee1214bf5af29b9d", size = 494761 }, + { url = "https://files.pythonhosted.org/packages/0c/ac/73816237dbec20a7299abf901e2f7b6061d238754e033b48e423603f5336/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4b90fc81411943b114e8eb4988a876ba3b12bd2d20741559803eddc4131575dc", size = 596141 }, + { url = "https://files.pythonhosted.org/packages/52/0a/dd48debf386a206ee1c6ad75a0827eac89428441291c90d98bc3803fccf1/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56cc2c2df742fa6529285b7d204720f34b7da789ed78eb578442f93c6de97d89", size = 541633 }, + { url = "https://files.pythonhosted.org/packages/92/ca/ebb707ab280636b8f46d040ccb051d1a9ddbc1f1ca2d90cdba626872f405/py_rust_stemmers-0.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd967eea2f808a1e73aa71ecccef0f4925a4cca4eb02ced94057afe3303153ef", size = 212134 }, + { url = "https://files.pythonhosted.org/packages/c2/98/f078f3930311e7b6154ccdf9166c4e30a416c7d199e136b5f09265d58a35/py_rust_stemmers-0.1.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5bd15b89203ecd886960e237124d1aa6e55498d76418c36c967d3b12168d43dc", size = 290427 }, + { url = "https://files.pythonhosted.org/packages/c9/46/21d784a3f1db6a23051ffd5826d8ee667d26a64587c1cfbda0443ed87fff/py_rust_stemmers-0.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c92733b020534470ca5a0d7fe8b85c85622ff383d4f37fec75a1c677aa84921", size = 275628 }, + { url = "https://files.pythonhosted.org/packages/57/d5/701c73a4f6a7fecfd96a6588f0cafe98d6b0acde93adf8a2e45535f3d1d5/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ab605a86c950ba7e8ab1392cf91296c0bec3084babb897a4aecf90a10c82395", size = 316656 }, + { url = "https://files.pythonhosted.org/packages/9d/0d/c58fe98153cfdb6abf4dfb6ac335c923000d4af4e736080c3a3045b7aea7/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21ed8055cec1f78d666afad8ffd7a51775ba419d2c615b8a1df7b32ca7f33e2b", size = 319377 }, + { url = "https://files.pythonhosted.org/packages/5c/d7/e60d04849e90aa3ad457211cc4999c30401f433341f9a5588c12b81f9877/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae773e1d01e9aa328d175f461475d0cd7074a82bfcc71de6dc5765e51f1cc9f7", size = 323719 }, + { url = "https://files.pythonhosted.org/packages/6a/48/c0e4fb955db784cc354e0756354602f7043ff4c10fcbd9d901a2f8fe3239/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5cc8fab9d0f1b274a26935a632362b8278f03e81b65e8b8644d5ca3f62a5a1a4", size = 324110 }, + { url = "https://files.pythonhosted.org/packages/48/eb/981b26baff37cf7a26ee206763cc4d2fb3e1db8f0f86ec030074431fae05/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35570098da02eb439afcd7270a12bf850bbe874b85cb912e0fb2d87a6e703920", size = 494645 }, + { url = "https://files.pythonhosted.org/packages/6d/af/f16e805b7aefc2257b192b83a89300c8360b0fdffd3dfefa92dee4ec9b15/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0a68745d4b3c7f5abc778ca967e8711df6154873abcfe4e62a6631fa2363cc32", size = 596124 }, + { url = "https://files.pythonhosted.org/packages/76/8c/e7a2c940ba00e0792ae346aed5e755d51d37cf6d6853f6b141e5380e285d/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7cc0cc0b8eb45d2158c28ea43e2f338c110aad63052ad3bd00bc7446a595e12f", size = 541771 }, + { url = "https://files.pythonhosted.org/packages/c2/a0/dd7c5fc6ade6d2a2a49e49937f06f2d488511454e8ab1b313d277ee8c3b1/py_rust_stemmers-0.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:15af4e12e1288de2e5241eec375afc6ad6be4c125a28ca010599d9f92db23f01", size = 212438 }, + { url = "https://files.pythonhosted.org/packages/b0/7e/f4346adfd44acbd7eaedcbd7d21b7f40ec9712e6c699e71fddad8dae6f8d/py_rust_stemmers-0.1.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:526b58958c6ffa36c4a805326cfb624ecbd665d16ba435027dbed0bcbcaa09d2", size = 290379 }, + { url = "https://files.pythonhosted.org/packages/c2/d8/988fc3f5dc0dbbd4bf5909f50ff953ab55ee8b5f79a835d00e57847d3123/py_rust_stemmers-0.1.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2b607f0b270951fb66479baf4b68716cc63a981585cbd898b0b6b5c359efde7e", size = 275458 }, + { url = "https://files.pythonhosted.org/packages/f4/94/e04c8b6a8364bca1b368785cef143755dd2d1ffe74df8f8b47b075bb1043/py_rust_stemmers-0.1.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0327b151ab8a338fb54fdac114ba34394327fc1e2c4c425ad1caf2013e5de3", size = 314711 }, + { url = "https://files.pythonhosted.org/packages/4f/cb/f59f9a80caa099cb6625a46c9a8e6e7e80bb3ed284f17e80245c8240a66e/py_rust_stemmers-0.1.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dadd0e369703817fc7026987b3093f461f9f58d8dde74e689d546184bc8f3451", size = 319370 }, + { url = "https://files.pythonhosted.org/packages/06/59/8211cd0f56e53f7770debd9a78de37985fb5662ae66e3b7b380f4c79888b/py_rust_stemmers-0.1.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245e2c61c52e073341893a9682cd1396b61047154548aee30bb1af3d8ed4b4cc", size = 321373 }, + { url = "https://files.pythonhosted.org/packages/10/72/fe33e614c114264d1ba54d39da4b5a4abeb6aedd0d26e5a8fd0637d6ddba/py_rust_stemmers-0.1.8-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:451ee1c02a3f5cf1e161b46ba9032cdda4ba10a8b03ff9ee61c1d34d42a0bc81", size = 321707 }, + { url = "https://files.pythonhosted.org/packages/91/f9/3cd18902fe2fa54557d3fe9132552256372d381c7aca71346163055d78b1/py_rust_stemmers-0.1.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d396dd25c473c1bc4248c79cd223f4b36356b55a124652f015c6a001547f81ac", size = 492457 }, + { url = "https://files.pythonhosted.org/packages/90/d7/32c6d3995e7036b73683389de2771f4dbbf40de192b7efe73c2528ee1eb5/py_rust_stemmers-0.1.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:479c77c32d8be692f3cfcde7e19273f02ac81d6f45c6aef49887ef95cab7abbb", size = 596085 }, + { url = "https://files.pythonhosted.org/packages/00/8c/e68fa5d862ea6a27fced3535c25ea4eaa26ba1ce00dfef5841924c74b167/py_rust_stemmers-0.1.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c786235275c5c2abb7f206b8236aee3ca0bc53c7497daf7fb7b01d3491469547", size = 539747 }, + { url = "https://files.pythonhosted.org/packages/44/48/aa584cf3772e01231641c95dc1aa73327a7d986c562639d78d0013733acf/py_rust_stemmers-0.1.8-cp314-cp314-win_amd64.whl", hash = "sha256:931d13570962b093417e5443a9d1bd63d73fa239ebb81e5b1d346663571403e4", size = 209636 }, ] [[package]] name = "pyasn1" version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997 }, ] [[package]] @@ -3020,48 +3290,48 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } 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" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, ] [[package]] name = "pycryptodome" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152 }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348 }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033 }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142 }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384 }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237 }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898 }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197 }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600 }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740 }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685 }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362 }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 }, ] [[package]] @@ -3074,9 +3344,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775 } 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" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262 }, ] [[package]] @@ -3086,72 +3356,72 @@ 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823 }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919 }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693 }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819 }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354 }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238 }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575 }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624 }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, ] [[package]] @@ -3163,9 +3433,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715 }, ] [[package]] @@ -3175,27 +3445,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } 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" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515 } 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" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274 }, ] [package.optional-dependencies] @@ -3214,24 +3484,24 @@ dependencies = [ { name = "python-xlib", marker = "'linux' in sys_platform" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/c6/e2d415610cfbc78308bee44218a46124aaa3301b1df08814df819b2254a1/pynput-1.8.2.tar.gz", hash = "sha256:f493c87157cd3861b4468f7f896857051762f44ed26f1b641e7cc5840a457087", size = 82818, upload-time = "2026-05-12T19:11:39.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/c6/e2d415610cfbc78308bee44218a46124aaa3301b1df08814df819b2254a1/pynput-1.8.2.tar.gz", hash = "sha256:f493c87157cd3861b4468f7f896857051762f44ed26f1b641e7cc5840a457087", size = 82818 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/98/bbeb760852adb27f166ce1617f0e51aabb15f21b1e60ea703f2aed3c78ac/pynput-1.8.2-py2.py3-none-any.whl", hash = "sha256:8cc38cf13a6ab2749cb375678be8a0fd705d7ce49c8001ff5db4007a723bbef1", size = 92028, upload-time = "2026-05-12T19:11:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/6d/98/bbeb760852adb27f166ce1617f0e51aabb15f21b1e60ea703f2aed3c78ac/pynput-1.8.2-py2.py3-none-any.whl", hash = "sha256:8cc38cf13a6ab2749cb375678be8a0fd705d7ce49c8001ff5db4007a723bbef1", size = 92028 }, ] [[package]] name = "pyobjc-core" version = "12.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383, upload-time = "2026-06-19T16:19:39.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347, upload-time = "2026-06-19T16:04:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1e/b9b0ddffae66996b8779f1f7958adc9f21c13a0448cd3be8d7fe589b5b0f/pyobjc_core-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af101222762665a4125157906cb4b23f5d5a63d3851d5e0504f72a1eaaa2cfd2", size = 6436004, upload-time = "2026-06-19T16:04:53.257Z" }, - { url = "https://files.pythonhosted.org/packages/8f/26/bd309ede07784c6e5fac4b440c90a5f72a66da7859ed303a9392fe8a5f3f/pyobjc_core-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:efe465e3ecc6fc73f7c7622620345d134a8d34564ab1c29d8247e45f4ed55071", size = 6687044, upload-time = "2026-06-19T16:04:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8a/cfa4f56939d554dbb342ec6e5226a441e2f552bc2002a0ddf7705bb11bef/pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a", size = 6429289, upload-time = "2026-06-19T16:05:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/42/74/446c89bc18103aaa4a00d1fb85ff8acace9a0dc3f362d9678ebf7571e275/pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7", size = 6690181, upload-time = "2026-06-19T16:05:06.201Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/0121ee4c616af07ad2de8cd1a286f6978dc9a227eb58b7c2e875cb68a1df/pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e", size = 6487078, upload-time = "2026-06-19T16:05:10.093Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a8/cb9fcc150f97d0bf22a2028f88b24cc35949beb1bcc7b8bc5c17d4401677/pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb", size = 6733064, upload-time = "2026-06-19T16:05:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347 }, + { url = "https://files.pythonhosted.org/packages/3e/1e/b9b0ddffae66996b8779f1f7958adc9f21c13a0448cd3be8d7fe589b5b0f/pyobjc_core-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af101222762665a4125157906cb4b23f5d5a63d3851d5e0504f72a1eaaa2cfd2", size = 6436004 }, + { url = "https://files.pythonhosted.org/packages/8f/26/bd309ede07784c6e5fac4b440c90a5f72a66da7859ed303a9392fe8a5f3f/pyobjc_core-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:efe465e3ecc6fc73f7c7622620345d134a8d34564ab1c29d8247e45f4ed55071", size = 6687044 }, + { url = "https://files.pythonhosted.org/packages/bd/8a/cfa4f56939d554dbb342ec6e5226a441e2f552bc2002a0ddf7705bb11bef/pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a", size = 6429289 }, + { url = "https://files.pythonhosted.org/packages/42/74/446c89bc18103aaa4a00d1fb85ff8acace9a0dc3f362d9678ebf7571e275/pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7", size = 6690181 }, + { url = "https://files.pythonhosted.org/packages/99/c7/0121ee4c616af07ad2de8cd1a286f6978dc9a227eb58b7c2e875cb68a1df/pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e", size = 6487078 }, + { url = "https://files.pythonhosted.org/packages/b5/a8/cb9fcc150f97d0bf22a2028f88b24cc35949beb1bcc7b8bc5c17d4401677/pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb", size = 6733064 }, ] [[package]] @@ -3244,15 +3514,15 @@ dependencies = [ { name = "pyobjc-framework-coretext" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/4d/0ebdd8144aba94b8fe9828ccee5616a4bf53d1f8bc51cff55f3cce86d695/pyobjc_framework_applicationservices-12.2.1.tar.gz", hash = "sha256:048ea663c9ac75c44a15dc7d5b8d78cbb4c97bf1c76e83835e8d5498e184001f", size = 109342, upload-time = "2026-06-19T16:19:46.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/4d/0ebdd8144aba94b8fe9828ccee5616a4bf53d1f8bc51cff55f3cce86d695/pyobjc_framework_applicationservices-12.2.1.tar.gz", hash = "sha256:048ea663c9ac75c44a15dc7d5b8d78cbb4c97bf1c76e83835e8d5498e184001f", size = 109342 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/89/39a7462006afbc06c69029fe4181b7359a9da25ae7864ef75f9d3ffb9272/pyobjc_framework_applicationservices-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f519ced13888d03410cd7da1f08fc56ee2944099e607216cef7ca26ecfdef61b", size = 32764, upload-time = "2026-06-19T16:05:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/8e928d5e3025529ed92c6eb5fd88a5e6e485cc6df945c541f29b4af7f2c6/pyobjc_framework_applicationservices-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8749290f796e6cca341d443769b79329dde5d157bcc4413c1f7fdb68ea4a8e48", size = 32782, upload-time = "2026-06-19T16:05:40.284Z" }, - { url = "https://files.pythonhosted.org/packages/50/a5/c7b5a31777fe2ce7c07b9c16941ff4fbf0a150bf755164d96228d32ccb4f/pyobjc_framework_applicationservices-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9ee11677fbd6a0987234814c7dde88ffd11242e8c1f76952e6654ea07f2370ac", size = 33048, upload-time = "2026-06-19T16:05:41.308Z" }, - { url = "https://files.pythonhosted.org/packages/8b/47/cd2bd76b862686c0aa78568ed9dff175764353c209a3096c72d6e2a9b151/pyobjc_framework_applicationservices-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a1c0ee536cb8bd7f5a811165ec323a9207b1e8dad9534fe2081f767fb90b0411", size = 32921, upload-time = "2026-06-19T16:05:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6989a96f8501aa3a16513d08b2c4c78ca906a10b6a4e5a33c4c59fd1bea9/pyobjc_framework_applicationservices-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0d55dd5be19e4a1363662bc8b48894d45714d07f0ee3958665fc9ea7df0f61b7", size = 33163, upload-time = "2026-06-19T16:05:43.256Z" }, - { url = "https://files.pythonhosted.org/packages/5c/41/66f2bcd12454a85f0479393f5825021332ee84b49583c7954cd20310cab5/pyobjc_framework_applicationservices-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e91c84238b2f68f608473854bcabb8770f15ad837e7561d217f24a1e482e42be", size = 32915, upload-time = "2026-06-19T16:05:44.151Z" }, - { url = "https://files.pythonhosted.org/packages/81/cf/04b6b1eb181fa3071e9743bab7551f5d2ec3f650ac74bab790f21717ba51/pyobjc_framework_applicationservices-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:09bfa27765d4c74155323fd8185b7aba6d639ce06c0a9f00ee2d9e7dce3d7800", size = 33158, upload-time = "2026-06-19T16:05:45.017Z" }, + { url = "https://files.pythonhosted.org/packages/bf/89/39a7462006afbc06c69029fe4181b7359a9da25ae7864ef75f9d3ffb9272/pyobjc_framework_applicationservices-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f519ced13888d03410cd7da1f08fc56ee2944099e607216cef7ca26ecfdef61b", size = 32764 }, + { url = "https://files.pythonhosted.org/packages/b2/6e/8e928d5e3025529ed92c6eb5fd88a5e6e485cc6df945c541f29b4af7f2c6/pyobjc_framework_applicationservices-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8749290f796e6cca341d443769b79329dde5d157bcc4413c1f7fdb68ea4a8e48", size = 32782 }, + { url = "https://files.pythonhosted.org/packages/50/a5/c7b5a31777fe2ce7c07b9c16941ff4fbf0a150bf755164d96228d32ccb4f/pyobjc_framework_applicationservices-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9ee11677fbd6a0987234814c7dde88ffd11242e8c1f76952e6654ea07f2370ac", size = 33048 }, + { url = "https://files.pythonhosted.org/packages/8b/47/cd2bd76b862686c0aa78568ed9dff175764353c209a3096c72d6e2a9b151/pyobjc_framework_applicationservices-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a1c0ee536cb8bd7f5a811165ec323a9207b1e8dad9534fe2081f767fb90b0411", size = 32921 }, + { url = "https://files.pythonhosted.org/packages/05/db/6989a96f8501aa3a16513d08b2c4c78ca906a10b6a4e5a33c4c59fd1bea9/pyobjc_framework_applicationservices-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0d55dd5be19e4a1363662bc8b48894d45714d07f0ee3958665fc9ea7df0f61b7", size = 33163 }, + { url = "https://files.pythonhosted.org/packages/5c/41/66f2bcd12454a85f0479393f5825021332ee84b49583c7954cd20310cab5/pyobjc_framework_applicationservices-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e91c84238b2f68f608473854bcabb8770f15ad837e7561d217f24a1e482e42be", size = 32915 }, + { url = "https://files.pythonhosted.org/packages/81/cf/04b6b1eb181fa3071e9743bab7551f5d2ec3f650ac74bab790f21717ba51/pyobjc_framework_applicationservices-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:09bfa27765d4c74155323fd8185b7aba6d639ce06c0a9f00ee2d9e7dce3d7800", size = 33158 }, ] [[package]] @@ -3262,15 +3532,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751, upload-time = "2026-06-19T16:20:05.159Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113, upload-time = "2026-06-19T16:07:38.9Z" }, - { url = "https://files.pythonhosted.org/packages/cc/46/68e8e4d926a2f70fed0437047bc3f9fe08af8fe620d94d80656ebc3cfa9b/pyobjc_framework_cocoa-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3b74a78fa7803e547b32e5e8ec1b49987b52fe318383e793bc6cd49b80efbd9f", size = 388183, upload-time = "2026-06-19T16:07:40.483Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f3/dfc9af4c9eb2e5389c860ad5ef252be9fe456db09f39d537555dc5057aa1/pyobjc_framework_cocoa-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dc2eaca2f13c7bcd8e41e51a372e47825dea9dd3126108760eed7ba883d2945c", size = 392275, upload-time = "2026-06-19T16:07:42.078Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c8/b90baa8f3592eded79b4be98fb59d2b8dc16b62361e34292bd95806ebd9f/pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561", size = 388357, upload-time = "2026-06-19T16:07:43.364Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/64a94651b9294702d55e748d94de30e25bc59d0784526be7643f4467eccd/pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b", size = 392404, upload-time = "2026-06-19T16:07:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/26e8a7bf1f5e8caa38b7f80d486296f9fd3c97e71ad7e5444ef22e802758/pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103", size = 388589, upload-time = "2026-06-19T16:07:46.276Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/eedf743a303ea742b8e082afe3613fb4d6618bc1a48cf2568b004ce906f7/pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1", size = 392691, upload-time = "2026-06-19T16:07:47.477Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113 }, + { url = "https://files.pythonhosted.org/packages/cc/46/68e8e4d926a2f70fed0437047bc3f9fe08af8fe620d94d80656ebc3cfa9b/pyobjc_framework_cocoa-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3b74a78fa7803e547b32e5e8ec1b49987b52fe318383e793bc6cd49b80efbd9f", size = 388183 }, + { url = "https://files.pythonhosted.org/packages/2e/f3/dfc9af4c9eb2e5389c860ad5ef252be9fe456db09f39d537555dc5057aa1/pyobjc_framework_cocoa-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dc2eaca2f13c7bcd8e41e51a372e47825dea9dd3126108760eed7ba883d2945c", size = 392275 }, + { url = "https://files.pythonhosted.org/packages/ec/c8/b90baa8f3592eded79b4be98fb59d2b8dc16b62361e34292bd95806ebd9f/pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561", size = 388357 }, + { url = "https://files.pythonhosted.org/packages/98/d8/64a94651b9294702d55e748d94de30e25bc59d0784526be7643f4467eccd/pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b", size = 392404 }, + { url = "https://files.pythonhosted.org/packages/5c/cc/26e8a7bf1f5e8caa38b7f80d486296f9fd3c97e71ad7e5444ef22e802758/pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103", size = 388589 }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eedf743a303ea742b8e082afe3613fb4d6618bc1a48cf2568b004ce906f7/pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1", size = 392691 }, ] [[package]] @@ -3282,15 +3552,15 @@ dependencies = [ { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/9c/4c7f452059dc1d3845b8e627b9113c247a997b9b07518e848c2ab7ff3149/pyobjc_framework_coretext-12.2.1.tar.gz", hash = "sha256:af740e784d7c592c34025ec7165f4f6c1a69b5a2d9075f06e41e4f77c212aed2", size = 97349, upload-time = "2026-06-19T16:20:22.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/9c/4c7f452059dc1d3845b8e627b9113c247a997b9b07518e848c2ab7ff3149/pyobjc_framework_coretext-12.2.1.tar.gz", hash = "sha256:af740e784d7c592c34025ec7165f4f6c1a69b5a2d9075f06e41e4f77c212aed2", size = 97349 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/11/c1298c2ec3b0cd19a457a1fd0da47898f894a13df5516f80dc04d1a7a4d9/pyobjc_framework_coretext-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac2ead13dfa4379a1566129d0e8a8ea778a2bcac9ac360a583360fd4f1ba39c6", size = 30123, upload-time = "2026-06-19T16:09:51.183Z" }, - { url = "https://files.pythonhosted.org/packages/05/8c/154e8f34923b24aade64a20eca2b759f8f67e109654308103080751f246f/pyobjc_framework_coretext-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5a3c6e2d905a17efb15572dad97ce582feeab5c3b92537015445e0e0bb46de", size = 30116, upload-time = "2026-06-19T16:09:52.219Z" }, - { url = "https://files.pythonhosted.org/packages/01/61/f53458c8f7fe74008e342946eca1fa82b777b284d4e13d8bd2e3e5724cab/pyobjc_framework_coretext-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5c979058c77df8cd3dac5fd7db4c484f9886fbe09e2687bfaf269a856f631f78", size = 30659, upload-time = "2026-06-19T16:09:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d8/d1178bb1ba3bb7a0d7a55db460aa89f2a8b232ed7eaf76cb402923cacf2d/pyobjc_framework_coretext-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d0b3b0467a23dbc2a39d0839e7100cc98b429fb7d52a471bd65477f46bb4c9e5", size = 30100, upload-time = "2026-06-19T16:09:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/12/c3/780d739909e6d8ddd9e8786fd19e8ea10ccfcc7275df987e349af0fb33b1/pyobjc_framework_coretext-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3d4a92fa657180cd0e900b98535da4f0f4d8c76a7077730a507e50d52d2853e3", size = 30642, upload-time = "2026-06-19T16:09:54.736Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/867197c6cf2396b33e95d6175d7fdc6f789314859fb107560ae9b19c7b14/pyobjc_framework_coretext-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:7a17034fd0a08cf58323b7234a385ce0ec355509d414df69e3f8f88df26de1fc", size = 30098, upload-time = "2026-06-19T16:09:55.679Z" }, - { url = "https://files.pythonhosted.org/packages/10/a3/f4e6d1a38cd4db8a1275eddb287f3cdc2c01c48f80b30e89cc58cfd92156/pyobjc_framework_coretext-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:28980144af75598654f6997b2bbb427885f4f5df90aa4d840f452ba5778ab155", size = 30669, upload-time = "2026-06-19T16:09:56.521Z" }, + { url = "https://files.pythonhosted.org/packages/c5/11/c1298c2ec3b0cd19a457a1fd0da47898f894a13df5516f80dc04d1a7a4d9/pyobjc_framework_coretext-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac2ead13dfa4379a1566129d0e8a8ea778a2bcac9ac360a583360fd4f1ba39c6", size = 30123 }, + { url = "https://files.pythonhosted.org/packages/05/8c/154e8f34923b24aade64a20eca2b759f8f67e109654308103080751f246f/pyobjc_framework_coretext-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5a3c6e2d905a17efb15572dad97ce582feeab5c3b92537015445e0e0bb46de", size = 30116 }, + { url = "https://files.pythonhosted.org/packages/01/61/f53458c8f7fe74008e342946eca1fa82b777b284d4e13d8bd2e3e5724cab/pyobjc_framework_coretext-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5c979058c77df8cd3dac5fd7db4c484f9886fbe09e2687bfaf269a856f631f78", size = 30659 }, + { url = "https://files.pythonhosted.org/packages/c6/d8/d1178bb1ba3bb7a0d7a55db460aa89f2a8b232ed7eaf76cb402923cacf2d/pyobjc_framework_coretext-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d0b3b0467a23dbc2a39d0839e7100cc98b429fb7d52a471bd65477f46bb4c9e5", size = 30100 }, + { url = "https://files.pythonhosted.org/packages/12/c3/780d739909e6d8ddd9e8786fd19e8ea10ccfcc7275df987e349af0fb33b1/pyobjc_framework_coretext-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3d4a92fa657180cd0e900b98535da4f0f4d8c76a7077730a507e50d52d2853e3", size = 30642 }, + { url = "https://files.pythonhosted.org/packages/0f/e9/867197c6cf2396b33e95d6175d7fdc6f789314859fb107560ae9b19c7b14/pyobjc_framework_coretext-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:7a17034fd0a08cf58323b7234a385ce0ec355509d414df69e3f8f88df26de1fc", size = 30098 }, + { url = "https://files.pythonhosted.org/packages/10/a3/f4e6d1a38cd4db8a1275eddb287f3cdc2c01c48f80b30e89cc58cfd92156/pyobjc_framework_coretext-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:28980144af75598654f6997b2bbb427885f4f5df90aa4d840f452ba5778ab155", size = 30669 }, ] [[package]] @@ -3301,15 +3571,15 @@ dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/2a8b84dbf1fe7c04dd96ea73d991678d4e09a909f51971ecc51629bb2ab4/pyobjc_framework_quartz-12.2.1.tar.gz", hash = "sha256:b3b8b6f71e66147f8ff9e6213864cc8527e3a0b1ee90835b93ce221f4802d9b0", size = 3215521, upload-time = "2026-06-19T16:21:30.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/2a8b84dbf1fe7c04dd96ea73d991678d4e09a909f51971ecc51629bb2ab4/pyobjc_framework_quartz-12.2.1.tar.gz", hash = "sha256:b3b8b6f71e66147f8ff9e6213864cc8527e3a0b1ee90835b93ce221f4802d9b0", size = 3215521 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/fc/d7c7b3134cdbd1a487f3f77b5be125d87a6c9e7d9411035739d99335cc0c/pyobjc_framework_quartz-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:de9c8cca7e95290c8d540466af11c7cdfe3a5458e6f56c34006d5b45243f9ed9", size = 219000, upload-time = "2026-06-19T16:16:04.29Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4b/861f91a1565d3189ee899e177b915551fb9a7e2ca25414025a8974f04e74/pyobjc_framework_quartz-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54c9bc7f507192691841ee4eba5bf36990b259df83ac728efed2d7ea1cd021e4", size = 219403, upload-time = "2026-06-19T16:16:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/b27010d2f288737f627f74be6d5549f49c841542365c84b9a3011fe39ce7/pyobjc_framework_quartz-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bfc0d2badd819823d21df8069dcf9544ce360ed747a8895c51bdb25d8d125f45", size = 224458, upload-time = "2026-06-19T16:16:07.252Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/85ffd9d433989205d572a50d625c63b29c05e0c5235a725f15ae1023672c/pyobjc_framework_quartz-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ceb56939c337b36d9d81185ade31f77dc52c85cf79bb16e53e9b32f54b6bb3f5", size = 219769, upload-time = "2026-06-19T16:16:08.814Z" }, - { url = "https://files.pythonhosted.org/packages/e2/d6/b917e4b63d72ea84a27121076f3033f23f6497c0e6ce8d304766c899897f/pyobjc_framework_quartz-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8105c98b798f2bf81c05c54bddeeadbf62f0b5dfec13bd6e719dd2cdf7e1cddf", size = 224717, upload-time = "2026-06-19T16:16:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/04/e2/f3c1ed3228f7430ef5ade23db6f1fcbae99290f177ce5653348fd9e05f4d/pyobjc_framework_quartz-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:bbc214f1a216b5d3651bc832d0ac4589f029f3f37cd6cbb370aac12a7c77942c", size = 219825, upload-time = "2026-06-19T16:16:11.433Z" }, - { url = "https://files.pythonhosted.org/packages/66/2a/2c99a5ad2fe0a11600ea123b8e9a08ff138fcb2ad1e13e376f4bd4aa1d96/pyobjc_framework_quartz-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:ca61624a0b0e6286d8a0f97f47eb9011e4e81e9a339db436d48af527e7065bb1", size = 224770, upload-time = "2026-06-19T16:16:13.035Z" }, + { url = "https://files.pythonhosted.org/packages/14/fc/d7c7b3134cdbd1a487f3f77b5be125d87a6c9e7d9411035739d99335cc0c/pyobjc_framework_quartz-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:de9c8cca7e95290c8d540466af11c7cdfe3a5458e6f56c34006d5b45243f9ed9", size = 219000 }, + { url = "https://files.pythonhosted.org/packages/0a/4b/861f91a1565d3189ee899e177b915551fb9a7e2ca25414025a8974f04e74/pyobjc_framework_quartz-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54c9bc7f507192691841ee4eba5bf36990b259df83ac728efed2d7ea1cd021e4", size = 219403 }, + { url = "https://files.pythonhosted.org/packages/ba/b5/b27010d2f288737f627f74be6d5549f49c841542365c84b9a3011fe39ce7/pyobjc_framework_quartz-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bfc0d2badd819823d21df8069dcf9544ce360ed747a8895c51bdb25d8d125f45", size = 224458 }, + { url = "https://files.pythonhosted.org/packages/8b/5d/85ffd9d433989205d572a50d625c63b29c05e0c5235a725f15ae1023672c/pyobjc_framework_quartz-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ceb56939c337b36d9d81185ade31f77dc52c85cf79bb16e53e9b32f54b6bb3f5", size = 219769 }, + { url = "https://files.pythonhosted.org/packages/e2/d6/b917e4b63d72ea84a27121076f3033f23f6497c0e6ce8d304766c899897f/pyobjc_framework_quartz-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8105c98b798f2bf81c05c54bddeeadbf62f0b5dfec13bd6e719dd2cdf7e1cddf", size = 224717 }, + { url = "https://files.pythonhosted.org/packages/04/e2/f3c1ed3228f7430ef5ade23db6f1fcbae99290f177ce5653348fd9e05f4d/pyobjc_framework_quartz-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:bbc214f1a216b5d3651bc832d0ac4589f029f3f37cd6cbb370aac12a7c77942c", size = 219825 }, + { url = "https://files.pythonhosted.org/packages/66/2a/2c99a5ad2fe0a11600ea123b8e9a08ff138fcb2ad1e13e376f4bd4aa1d96/pyobjc_framework_quartz-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:ca61624a0b0e6286d8a0f97f47eb9011e4e81e9a339db436d48af527e7065bb1", size = 224770 }, ] [[package]] @@ -3320,36 +3590,45 @@ dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, + { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008 }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781 }, +] + +[[package]] +name = "pypdf" +version = "6.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123 }, ] [[package]] name = "pyproject-hooks" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, ] [[package]] name = "pyrfc3339" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589, upload-time = "2025-08-23T16:40:31.889Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, + { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758 }, ] [[package]] @@ -3363,9 +3642,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } 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" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, ] [[package]] @@ -3376,9 +3655,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514 } 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" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930 }, ] [[package]] @@ -3390,9 +3669,22 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 }, +] + +[[package]] +name = "pytest-testmon" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/1d/3e4230cc67cd6205bbe03c3527500c0ccaf7f0c78b436537eac71590ee4a/pytest_testmon-2.2.0.tar.gz", hash = "sha256:01f488e955ed0e0049777bee598bf1f647dd524e06f544c31a24e68f8d775a51", size = 23108, upload-time = "2025-12-01T07:30:24.76Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, + { url = "https://files.pythonhosted.org/packages/61/55/ebb3c2f59fb089f08d00f764830d35780fc4e4c41dffcadafa3264682b65/pytest_testmon-2.2.0-py3-none-any.whl", hash = "sha256:2604ca44a54d61a2e830d9ce828b41a837075e4ebc1f81b148add8e90d34815b", size = 25199, upload-time = "2025-12-01T07:30:23.623Z" }, ] [[package]] @@ -3402,9 +3694,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 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" }, + { 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 }, ] [[package]] @@ -3415,18 +3707,18 @@ dependencies = [ { name = "lxml" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } 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" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, ] [[package]] @@ -3436,18 +3728,33 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "simple-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/a0/f75491f942184d9960b15e763270f765fe9f239745ca5f9e16289011aed4/python_engineio-4.13.3.tar.gz", hash = "sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9", size = 79734, upload-time = "2026-06-20T22:53:52.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a0/f75491f942184d9960b15e763270f765fe9f239745ca5f9e16289011aed4/python_engineio-4.13.3.tar.gz", hash = "sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9", size = 79734 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/96/82f6328e410515fab21d5602ba35b9377a47b5a141a0c1f9efa00ce21eb4/python_engineio-4.13.3-py3-none-any.whl", hash = "sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f", size = 59993, upload-time = "2026-06-20T22:53:50.775Z" }, + { url = "https://files.pythonhosted.org/packages/5b/96/82f6328e410515fab21d5602ba35b9377a47b5a141a0c1f9efa00ce21eb4/python_engineio-4.13.3-py3-none-any.whl", hash = "sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f", size = 59993 }, ] [[package]] name = "python-multipart" version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042 }, +] + +[[package]] +name = "python-pptx" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788 }, ] [[package]] @@ -3458,9 +3765,9 @@ dependencies = [ { name = "bidict" }, { name = "python-engineio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/2d/ffce71017c106b75099fea569df6518c63fee5d6202ce0cfe7b01e6f22c3/python_socketio-5.16.3.tar.gz", hash = "sha256:89b136f677ae65607a84cecda9b4d6c5377b40a97582c504c25df89af16d520e", size = 128095, upload-time = "2026-06-15T22:07:04.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/2d/ffce71017c106b75099fea569df6518c63fee5d6202ce0cfe7b01e6f22c3/python_socketio-5.16.3.tar.gz", hash = "sha256:89b136f677ae65607a84cecda9b4d6c5377b40a97582c504c25df89af16d520e", size = 128095 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/38/8c5e72d53ff8eb27497c4f268a7f6d9121e727a50b65248288ad79a93053/python_socketio-5.16.3-py3-none-any.whl", hash = "sha256:e7ad14202a5e6448824c7c2f86161d04e13dec05992257df5c709e6a2798c041", size = 82087, upload-time = "2026-06-15T22:07:02.498Z" }, + { url = "https://files.pythonhosted.org/packages/0a/38/8c5e72d53ff8eb27497c4f268a7f6d9121e727a50b65248288ad79a93053/python_socketio-5.16.3-py3-none-any.whl", hash = "sha256:e7ad14202a5e6448824c7c2f86161d04e13dec05992257df5c709e6a2798c041", size = 82087 }, ] [package.optional-dependencies] @@ -3476,9 +3783,9 @@ dependencies = [ { name = "httpcore", marker = "python_full_version >= '3.14'" }, { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/77/153517bb1ac1bba670c6fb1dbf09e1fd0730494b1705934e715391413a0d/python_telegram_bot-22.8.tar.gz", hash = "sha256:f9d3847fcb23ee603477e442800b33bb4adf851a73e0619d2050be879decf1ef", size = 1551700, upload-time = "2026-06-12T08:10:29.1Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/77/153517bb1ac1bba670c6fb1dbf09e1fd0730494b1705934e715391413a0d/python_telegram_bot-22.8.tar.gz", hash = "sha256:f9d3847fcb23ee603477e442800b33bb4adf851a73e0619d2050be879decf1ef", size = 1551700 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/7c/ed7d4dd94280bd434173cae9f7a7aedaaab9af128ae4f494423a5687c820/python_telegram_bot-22.8-py3-none-any.whl", hash = "sha256:42373918097f1b837cc4e717d588c19ea79651497ec712bb5b0c76e5e63c50e1", size = 769397, upload-time = "2026-06-12T08:10:27.066Z" }, + { url = "https://files.pythonhosted.org/packages/60/7c/ed7d4dd94280bd434173cae9f7a7aedaaab9af128ae4f494423a5687c820/python_telegram_bot-22.8-py3-none-any.whl", hash = "sha256:42373918097f1b837cc4e717d588c19ea79651497ec712bb5b0c76e5e63c50e1", size = 769397 }, ] [[package]] @@ -3488,9 +3795,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185 }, ] [[package]] @@ -3498,64 +3805,64 @@ name = "pywin32" version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, - { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, - { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, - { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877 }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841 }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901 }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184 }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298 }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640 }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928 }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157 }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598 }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159 }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293 }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337 }, ] [[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" }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, ] [[package]] @@ -3565,9 +3872,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "prompt-toolkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753 }, ] [[package]] @@ -3579,97 +3886,97 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, ] [[package]] name = "regex" version = "2026.6.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/21/44aa415873032056c43eac21c67285deb2cf66cddb2a964c3cdc8f803efc/regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1", size = 490480, upload-time = "2026-06-28T19:54:05.392Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5f/30d4116093c2128099f78b6990dfc1698fdbf3ee528f1e1c647378034c79/regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9", size = 292137, upload-time = "2026-06-28T19:54:07.088Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/ca20a0e0de49837e6337603a91ab77556aa27033ac5b975615d98698cfb3/regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab", size = 289623, upload-time = "2026-06-28T19:54:08.762Z" }, - { url = "https://files.pythonhosted.org/packages/50/11/c013422a7e2c59946df8ac93e792a4922c98287f2a2181341603c78a5d98/regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195", size = 796756, upload-time = "2026-06-28T19:54:10.616Z" }, - { url = "https://files.pythonhosted.org/packages/b0/95/1309645a0e1ee6fb91d954501da57a0b33d50ad2a9acb313702851a7054e/regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf", size = 865465, upload-time = "2026-06-28T19:54:12.742Z" }, - { url = "https://files.pythonhosted.org/packages/20/06/491802db47c6f5e2904ffa2518ad3ac27fe6bbf5a66d73210a95cc080d47/regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2", size = 912350, upload-time = "2026-06-28T19:54:14.508Z" }, - { url = "https://files.pythonhosted.org/packages/5e/60/3ba57840bcc7e2367090360de0c15a5ba6ad22be89314251105f2e943f43/regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38", size = 801261, upload-time = "2026-06-28T19:54:16.432Z" }, - { url = "https://files.pythonhosted.org/packages/eb/27/af1eb74e9a78c782b3e450b611a595e44906da8a5107e1227f4a7fd0480b/regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3", size = 777072, upload-time = "2026-06-28T19:54:18.128Z" }, - { url = "https://files.pythonhosted.org/packages/20/18/fdd4c883a39e3ed00d669062af1135809bfd3281bf528150849fbd68825b/regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417", size = 785119, upload-time = "2026-06-28T19:54:20.314Z" }, - { url = "https://files.pythonhosted.org/packages/1c/79/0aabe34b8482dcadf64355f70f96e22eba5ec6c1efb33563f89654f4061c/regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e", size = 860118, upload-time = "2026-06-28T19:54:22.368Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2c/c973323306a27c9db7d160e9584eb7e0ece2a96224ccb0d39060558b31f9/regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8", size = 765786, upload-time = "2026-06-28T19:54:24.265Z" }, - { url = "https://files.pythonhosted.org/packages/e3/df/9ca3e378e352242a4cb45573a5e9162c3ee791507702a23966fa559e36b5/regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a", size = 852120, upload-time = "2026-06-28T19:54:25.972Z" }, - { url = "https://files.pythonhosted.org/packages/a2/3e/3e31e255c4971f53cbce6306b5e3c76cbd3735a54f419bb3b2f194e9f68c/regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8", size = 789503, upload-time = "2026-06-28T19:54:27.678Z" }, - { url = "https://files.pythonhosted.org/packages/72/01/d36561c21c3033d7eeb31d51b491916817de7861acefccc5fc9db8a5037c/regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463", size = 267109, upload-time = "2026-06-28T19:54:29.316Z" }, - { url = "https://files.pythonhosted.org/packages/a0/59/bbbb0591f38b18c65977cd65ce64749eba1c1996c99ac04e900fc30c0dcb/regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84", size = 277711, upload-time = "2026-06-28T19:54:31.143Z" }, - { url = "https://files.pythonhosted.org/packages/86/06/be4f6b337d773ae5739a1bc238f97c16926e72017243735853c030f4c628/regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6", size = 277022, upload-time = "2026-06-28T19:54:32.97Z" }, - { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" }, - { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" }, - { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" }, - { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" }, - { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" }, - { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" }, - { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" }, - { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" }, - { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" }, - { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" }, - { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" }, - { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" }, - { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" }, - { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" }, - { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" }, - { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" }, - { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" }, - { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" }, - { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" }, - { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" }, - { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" }, - { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" }, - { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" }, - { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" }, - { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" }, - { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" }, - { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" }, - { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" }, - { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" }, - { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" }, - { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" }, - { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" }, - { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" }, - { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" }, - { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" }, - { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/21/44aa415873032056c43eac21c67285deb2cf66cddb2a964c3cdc8f803efc/regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1", size = 490480 }, + { url = "https://files.pythonhosted.org/packages/8b/5f/30d4116093c2128099f78b6990dfc1698fdbf3ee528f1e1c647378034c79/regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9", size = 292137 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/ca20a0e0de49837e6337603a91ab77556aa27033ac5b975615d98698cfb3/regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab", size = 289623 }, + { url = "https://files.pythonhosted.org/packages/50/11/c013422a7e2c59946df8ac93e792a4922c98287f2a2181341603c78a5d98/regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195", size = 796756 }, + { url = "https://files.pythonhosted.org/packages/b0/95/1309645a0e1ee6fb91d954501da57a0b33d50ad2a9acb313702851a7054e/regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf", size = 865465 }, + { url = "https://files.pythonhosted.org/packages/20/06/491802db47c6f5e2904ffa2518ad3ac27fe6bbf5a66d73210a95cc080d47/regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2", size = 912350 }, + { url = "https://files.pythonhosted.org/packages/5e/60/3ba57840bcc7e2367090360de0c15a5ba6ad22be89314251105f2e943f43/regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38", size = 801261 }, + { url = "https://files.pythonhosted.org/packages/eb/27/af1eb74e9a78c782b3e450b611a595e44906da8a5107e1227f4a7fd0480b/regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3", size = 777072 }, + { url = "https://files.pythonhosted.org/packages/20/18/fdd4c883a39e3ed00d669062af1135809bfd3281bf528150849fbd68825b/regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417", size = 785119 }, + { url = "https://files.pythonhosted.org/packages/1c/79/0aabe34b8482dcadf64355f70f96e22eba5ec6c1efb33563f89654f4061c/regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e", size = 860118 }, + { url = "https://files.pythonhosted.org/packages/a8/2c/c973323306a27c9db7d160e9584eb7e0ece2a96224ccb0d39060558b31f9/regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8", size = 765786 }, + { url = "https://files.pythonhosted.org/packages/e3/df/9ca3e378e352242a4cb45573a5e9162c3ee791507702a23966fa559e36b5/regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a", size = 852120 }, + { url = "https://files.pythonhosted.org/packages/a2/3e/3e31e255c4971f53cbce6306b5e3c76cbd3735a54f419bb3b2f194e9f68c/regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8", size = 789503 }, + { url = "https://files.pythonhosted.org/packages/72/01/d36561c21c3033d7eeb31d51b491916817de7861acefccc5fc9db8a5037c/regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463", size = 267109 }, + { url = "https://files.pythonhosted.org/packages/a0/59/bbbb0591f38b18c65977cd65ce64749eba1c1996c99ac04e900fc30c0dcb/regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84", size = 277711 }, + { url = "https://files.pythonhosted.org/packages/86/06/be4f6b337d773ae5739a1bc238f97c16926e72017243735853c030f4c628/regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6", size = 277022 }, + { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329 }, + { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039 }, + { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488 }, + { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772 }, + { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467 }, + { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345 }, + { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291 }, + { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106 }, + { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175 }, + { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186 }, + { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754 }, + { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085 }, + { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600 }, + { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088 }, + { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680 }, + { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017 }, + { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195 }, + { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976 }, + { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340 }, + { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704 }, + { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157 }, + { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287 }, + { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333 }, + { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518 }, + { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371 }, + { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517 }, + { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834 }, + { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606 }, + { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475 }, + { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126 }, + { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961 }, + { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266 }, + { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407 }, + { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988 }, + { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704 }, + { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017 }, + { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112 }, + { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554 }, + { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665 }, + { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243 }, + { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784 }, + { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914 }, + { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915 }, + { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404 }, + { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373 }, + { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496 }, + { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754 }, + { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979 }, + { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282 }, + { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977 }, + { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432 }, + { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877 }, + { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212 }, + { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507 }, + { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389 }, + { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890 }, + { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451 }, + { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504 }, + { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047 }, + { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665 }, + { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573 }, + { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515 }, + { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650 }, + { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338 }, ] [[package]] @@ -3682,9 +3989,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, ] [[package]] @@ -3694,9 +4001,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, ] [[package]] @@ -3707,130 +4014,130 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680 } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 }, ] [[package]] name = "rpds-py" version = "2026.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, - { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, - { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, - { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, - { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, - { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, - { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, - { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, - { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, - { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, - { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, - { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, - { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, - { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, - { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, - { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, - { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, - { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, - { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, - { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, - { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, - { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, - { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, - { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, - { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, - { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, - { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, - { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, - { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, - { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691 }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542 }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180 }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067 }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509 }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754 }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189 }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750 }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576 }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807 }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187 }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030 }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185 }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394 }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753 }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012 }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203 }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984 }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815 }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545 }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828 }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678 }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811 }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382 }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832 }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011 }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431 }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710 }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454 }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063 }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510 }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495 }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454 }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583 }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919 }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255 }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060 }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960 }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356 }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319 }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508 }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504 }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380 }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976 }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840 }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282 }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403 }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055 }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419 }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848 }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369 }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673 }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500 }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978 }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350 }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486 }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068 }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600 }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726 }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587 }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585 }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479 }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418 }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123 }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351 }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827 }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966 }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680 }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853 }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715 }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864 }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430 }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877 }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933 }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274 }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763 }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467 }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689 }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340 }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179 }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993 }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909 }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584 }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357 }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533 }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204 }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719 }, ] [[package]] name = "ruff" version = "0.15.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489 } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665 }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649 }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638 }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227 }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882 }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808 }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094 }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176 }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767 }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132 }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828 }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418 }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770 }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698 }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322 }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274 }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498 }, ] [[package]] @@ -3840,27 +4147,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547 } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712 }, ] [[package]] name = "scalar-fastapi" version = "1.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/81/c0c70776a3d7d371ee06d38d26a8d361c97439d46f79acb6d67cf6c760ad/scalar_fastapi-1.8.2.tar.gz", hash = "sha256:0de09b8c63f78c1052792faa200d740b2ccaeeb88ac54e7ea633ac4edc6fde82", size = 8371, upload-time = "2026-04-09T22:41:24.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/81/c0c70776a3d7d371ee06d38d26a8d361c97439d46f79acb6d67cf6c760ad/scalar_fastapi-1.8.2.tar.gz", hash = "sha256:0de09b8c63f78c1052792faa200d740b2ccaeeb88ac54e7ea633ac4edc6fde82", size = 8371 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/b1/b9a482620479b2801a4dfa5c4c10b4cc280097e894114e0298f21a0fca55/scalar_fastapi-1.8.2-py3-none-any.whl", hash = "sha256:d96e2c8b3676491eaebb4ec8d9f4de77adb2374d86f87d321546fa6f084e8cb8", size = 7677, upload-time = "2026-04-09T22:41:23.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b1/b9a482620479b2801a4dfa5c4c10b4cc280097e894114e0298f21a0fca55/scalar_fastapi-1.8.2-py3-none-any.whl", hash = "sha256:d96e2c8b3676491eaebb4ec8d9f4de77adb2374d86f87d321546fa6f084e8cb8", size = 7677 }, ] [[package]] name = "segno" version = "1.6.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/2e/b396f750c53f570055bf5a9fc1ace09bed2dff013c73b7afec5702a581ba/segno-1.6.6.tar.gz", hash = "sha256:e60933afc4b52137d323a4434c8340e0ce1e58cec71439e46680d4db188f11b3", size = 1628586, upload-time = "2025-03-12T22:12:53.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/2e/b396f750c53f570055bf5a9fc1ace09bed2dff013c73b7afec5702a581ba/segno-1.6.6.tar.gz", hash = "sha256:e60933afc4b52137d323a4434c8340e0ce1e58cec71439e46680d4db188f11b3", size = 1628586 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503, upload-time = "2025-03-12T22:12:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503 }, ] [[package]] @@ -3870,36 +4177,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wsproto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 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" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 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" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "soupsieve" version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304 }, ] [[package]] @@ -3910,37 +4217,37 @@ 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/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, - { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291 }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835 }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470 }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874 }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692 }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674 }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670 }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184 }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735 }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756 }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055 }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850 }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721 }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615 }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999 }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539 }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545 }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929 }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888 }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551 }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318 }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920 }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534 }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844 }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355 }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591 }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313 }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280 }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334 }, ] [[package]] @@ -3948,11 +4255,11 @@ name = "sqlite-vec" version = "0.1.9" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, - { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, - { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171 }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434 }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076 }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388 }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804 }, ] [[package]] @@ -3963,9 +4270,9 @@ dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518 }, ] [[package]] @@ -3976,27 +4283,27 @@ dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632 }, ] [[package]] name = "tabulate" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814 }, ] [[package]] name = "tenacity" version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926 }, ] [[package]] @@ -4007,43 +4314,70 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, - { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, - { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, - { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, - { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, - { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, - { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, - { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, - { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, - { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, - { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, - { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, - { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888 }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970 }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741 }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523 }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954 }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069 }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748 }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228 }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978 }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355 }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415 }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879 }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829 }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600 }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516 }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518 }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867 }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826 }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489 }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820 }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977 }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635 }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036 }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544 }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217 }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404 }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686 }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454 }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976 }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526 }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466 }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863 }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218 }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110 }, +] + +[[package]] +name = "tokenizers" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732 }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954 }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081 }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641 }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624 }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062 }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098 }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235 }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398 }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986 }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181 }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853 }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263 }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223 }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127 }, ] [[package]] @@ -4053,18 +4387,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337 }, ] [[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, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] [[package]] @@ -4074,18 +4408,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, ] [[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, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254 } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321 }, ] [[package]] @@ -4095,92 +4429,92 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115 }, ] [[package]] name = "uritemplate" version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488 }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, ] [[package]] name = "uuid-utils" version = "0.16.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/5a/5da7ae85b38e3eddba0be3e8e4328f90882fe92989728e6fb552963d4c42/uuid_utils-0.16.2.tar.gz", hash = "sha256:fa637e4f314ad5b59ff6d8e809d506443d68bef30bfaecdfcfe02cce689abb2f", size = 42962, upload-time = "2026-06-18T13:36:48.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/07/294b72a572218bf6e92355203b832b3356c58a7e1e0b92a034497d15bef9/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f064dc54c6abecb09eb104d953bfb079f3c395e0d6b18899979f852d1083549", size = 560726, upload-time = "2026-06-18T13:35:21.053Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3c/1095b6ab574a7fa69136d47bab5a43f320a8f00a0ecb96059fd49b1747b2/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:dd7aa18db5cc826d482d876a826fee445839701f81f78567e7c74b4458d57a84", size = 288065, upload-time = "2026-06-18T13:35:22.547Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/6404d48fe71def0733c9568d96043b2e1945e2e4205c4eb525db3da42ba3/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc25ad320c9b44c2d3ed33aff4f85b0b277bef4ff79b12c01ee58b52ea44be1d", size = 322946, upload-time = "2026-06-18T13:35:23.648Z" }, - { url = "https://files.pythonhosted.org/packages/74/00/8a009762015a134aa04b5451400e0ec9832ccd598ed4845f9aecb0be6299/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0ca752d51d1004caff65fccffd44b32a26cb099b546e0512cfa09facb683d6c", size = 330186, upload-time = "2026-06-18T13:35:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b0/1613bb98ac11234145aa5bc1de618be536818fef05dec595efb3e2b37097/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8323136bb02355c1b973492ab98b0722206dfdedfb148e4115c35fcdf3889bad", size = 444583, upload-time = "2026-06-18T13:35:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/93/66/83e62c7a152bbbb8b30ac58eaad81f3860ba2fba91a334c50f223f9ce878/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf8bfdffb22f620635580b17fd178272f30a9841b824b19b935c8db64bf09b6", size = 323064, upload-time = "2026-06-18T13:35:27.356Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/c1b2faaf3a9d7952f321a9fee3ad74e05b25878bd9b7cd6b0398fe77f279/uuid_utils-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61454f2139424a6cff14eca7849c28b3350f261453b74075aa20fe99592dbb16", size = 347967, upload-time = "2026-06-18T13:35:28.538Z" }, - { url = "https://files.pythonhosted.org/packages/24/d8/cdf79b242e41ae47b7cd617ac5d48f15ce44e81da8000379c757091ae5f8/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:725110434a1d482a639a9ac467a24f1cb531d84ab52e454a13fe145b10b42cae", size = 499187, upload-time = "2026-06-18T13:35:30.042Z" }, - { url = "https://files.pythonhosted.org/packages/be/10/978d5ad82bc0fe7ff02d5be6f1eb83b090849f0a95bf8438593565273b7a/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8197870739a3094990743a80f075fa0b17beafd6c187e5f360e021d90a12a6d1", size = 605696, upload-time = "2026-06-18T13:35:31.289Z" }, - { url = "https://files.pythonhosted.org/packages/3a/28/e382ee44a592e35b80397b493bf3fbbdb8e30a64eaaefc7dabc246aeb253/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e10a02b3a31ed44c7c9a96abde335f5fa222735e73f3081d693414377eb3b016", size = 564975, upload-time = "2026-06-18T13:35:32.419Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d0/f6011dbe4e5d751a8494715e014019cb5b242d8cd6dbec1cfec3d3fb2e81/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd32dbca0792b9683160151dc07fad11b915020eed7c82b43faf0862c2ff06a0", size = 528462, upload-time = "2026-06-18T13:35:33.685Z" }, - { url = "https://files.pythonhosted.org/packages/42/7f/279e6159c37f43feb9dd70218b49a26696cefddaef1db7f4b79895eaf5d5/uuid_utils-0.16.2-cp312-cp312-win32.whl", hash = "sha256:dcdfcab60562d12dd43c1a6f495b1d089e41f0e10fac37d94db285d72b678c23", size = 167047, upload-time = "2026-06-18T13:35:34.862Z" }, - { url = "https://files.pythonhosted.org/packages/47/38/f72f7bed062601448ec2db47351e6c1faccd78fd693bbc6e067299d1fa11/uuid_utils-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:97ee6f5e803ea571f5f6da42efc97d8c5a13f121043680177f8470529b94e855", size = 173821, upload-time = "2026-06-18T13:35:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/37/61/8a025284a31c85b7c0c5319e96868c2c09dea3fc5f676c979a4cd4baf2e7/uuid_utils-0.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:72cfd9ff1e8a7c371a044687e77eb873721c4a9f4814e453439bfba595b84303", size = 172206, upload-time = "2026-06-18T13:35:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/3b48859953ee74fc26628ca5d9e5f848209655a0a8c934032fc596035976/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c19b7d595d12923da682ed13d313c2333b9ebf214e65a47a24927a8a3a81b191", size = 560753, upload-time = "2026-06-18T13:35:38.531Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1c/77635489de5454f2a25411030f78d31931dbdc0c86114da00adb9b91f120/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:909e26fa2451c8db31b9ed1d3c8e4ecf513b6d1619db4205997fe99eb6b4ef4f", size = 288056, upload-time = "2026-06-18T13:35:39.966Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0e/8e799537ea458abaefb0f5c3b3b05304d3faf413feb0997605a3f8ae2484/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27271b37fbc6812bb1542c4b8e22ee00223a6bf7f62b1f38d3bcf8e92f6d9acd", size = 323196, upload-time = "2026-06-18T13:35:41.534Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/4e5b412d4710617fb83ed77b361f5fa6247b99bde2fa6ee07ddf851b59d1/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc4b9d96a2c689d664cf3fc7f7db46b82d2821fb2ce8a4f0798fc0a92c1569f8", size = 330858, upload-time = "2026-06-18T13:35:42.709Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e3/8173202b7cfcfeb4a588c5f8b85d3e2b44973384eb33167ee25c5c78867f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3bf41b696b0fe808df1b4091c70273a52ea033b0fe97341cd67ecd76d22bb3a", size = 444813, upload-time = "2026-06-18T13:35:43.917Z" }, - { url = "https://files.pythonhosted.org/packages/37/0d/c3918356932ce467b11e954d0c93697fb4652cf664957e3d9521f7ece22f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc329be41bb6534ecb03e50596179ab76c7643ced33d13c66967d5ae1869663", size = 322828, upload-time = "2026-06-18T13:35:45.134Z" }, - { url = "https://files.pythonhosted.org/packages/f0/80/4020556682441b62a25b7d07798812115fca97d417a3498d5af6dce36504/uuid_utils-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4125bf6ed3ae443c05e140f8585d174b9d647295b12034d5ec94ae2ae38edefa", size = 347909, upload-time = "2026-06-18T13:35:46.364Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/a1e87e268df98f6740af81abf225532c173a971c64df0258c84b630e35a7/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:840b21e609a9b203eee06bdc73e18397154447a9814a8e78d9b68e5104d9802f", size = 499469, upload-time = "2026-06-18T13:35:47.584Z" }, - { url = "https://files.pythonhosted.org/packages/25/75/5a1f297a09556c27d9617c44ab0510de5f3a70120df236f66b9d0fdd1976/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5119bec75f56bd028d97472f72b1ed723a0d60b09a48017dc70a3cb1892ed081", size = 606160, upload-time = "2026-06-18T13:35:48.963Z" }, - { url = "https://files.pythonhosted.org/packages/7c/de/140f1d2a161320d1ac9073a03b9eb31fe35ae70f56f8971ec1fb45c14a44/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9fe600ab7d3d4eb56986e814042c917e728ac92cd8a41f099a6b59b84d8bf9e6", size = 564856, upload-time = "2026-06-18T13:35:50.244Z" }, - { url = "https://files.pythonhosted.org/packages/01/3b/9a5fe6691f8f6d72899cdc2713ffbd845b8c6981eeeab66d98a71b721116/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44020a4532229ccfbba353138539774686350dda71cf4368e257973dd8ba403", size = 528376, upload-time = "2026-06-18T13:35:51.825Z" }, - { url = "https://files.pythonhosted.org/packages/87/ad/47c93dcabd00f6749803a00be361c75d7079c78ad5e67077dee63d30b687/uuid_utils-0.16.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:280d4f1f22dd2e79c1cc31ffc7fc26dc3534ffc114dedcdd29cc8489c5ce9c98", size = 98033, upload-time = "2026-06-18T13:35:53.385Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fd/8de85eeb8dd59354ad46e897ab0d0f0fe6bc48702239a6c9f2613f961c8e/uuid_utils-0.16.2-cp313-cp313-win32.whl", hash = "sha256:4942b26ad12c5187bac52b7fb4685040139ff0df9a19cde33e5025326f6180fc", size = 167054, upload-time = "2026-06-18T13:35:54.495Z" }, - { url = "https://files.pythonhosted.org/packages/86/b3/b5ba393fbe5142eb9d5db23d4b9b16dde2a4e1aee6f2fcb7fadef97e419a/uuid_utils-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:01f81c71cf2185de0707e9d2f248e17025ba50af0acd3cbf51cd8aea96c2e0be", size = 173481, upload-time = "2026-06-18T13:35:55.684Z" }, - { url = "https://files.pythonhosted.org/packages/b2/79/4e5d63d605b13201ae9af6fcc36ec77949cccc99486c430c016d8f8ed274/uuid_utils-0.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:c1dbe65ce6d46c5f645356d64bfb2de7564e2426ca8c9b1a0a401d6f7ae5cc22", size = 172197, upload-time = "2026-06-18T13:35:56.817Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/0e5a0c1e1e3243cf5f12efd2b88a33e63c38b6a79483d3c84b2f5e7265cf/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:617955f4b3f649617c0388127d8a257202189d5cc3c720313f8b207df1cdb2a4", size = 566227, upload-time = "2026-06-18T13:35:57.925Z" }, - { url = "https://files.pythonhosted.org/packages/28/b3/2b6f9d6832e939aaf2b2ba89ff70b3994cfa3ae9b14daac3329eb9202ef8/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0aa2569908bdb21ccb216cd6bd06cb934351ee65ea7cd5e351e19f633a99b577", size = 290301, upload-time = "2026-06-18T13:35:59.467Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/8bb31429884b9f340f964ed70b68bfd81cec61f6e6877633f6a014358e78/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4af7673e84e1ec6029f18d3a0408095c471c4e2691b6e46b4e1f0a2051734ba", size = 325409, upload-time = "2026-06-18T13:36:00.786Z" }, - { url = "https://files.pythonhosted.org/packages/1f/87/3b59aa97e788ca4fa46e2a3856ef567b51e03fd7fbf27d39ce36e46478b6/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecadf55ed6b8fb72e7966b52fd02919e7d7bb8e7bffeaf285803b82e774debfb", size = 332071, upload-time = "2026-06-18T13:36:02.043Z" }, - { url = "https://files.pythonhosted.org/packages/1c/21/8c21bf6cf3ce9447b73cee6a38ca63c9bb2f3145259422646bae8e8ddc21/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:026b96b2f1e6b004579e030692d2f6568ccd0b29d40687213c31694abf570c78", size = 447075, upload-time = "2026-06-18T13:36:03.305Z" }, - { url = "https://files.pythonhosted.org/packages/95/43/77e83019effe1a5ab7169a2d4bf1bd654bebd850b81c8a937b96bd6b5c9c/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273679723e88544dd2de0564ab7f2fddfa2270faf05cabfdf63c275be67ec2a1", size = 325061, upload-time = "2026-06-18T13:36:04.972Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a6/7bf6e0165dc191c09bc4e8c011de5463d64c5a651ed38ad6698bfc552a52/uuid_utils-0.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5b1a338b92d1eb121e9eaf06ae3db1b9a5cd794ce318a475f6dc6f9e89c3a8", size = 350302, upload-time = "2026-06-18T13:36:06.172Z" }, - { url = "https://files.pythonhosted.org/packages/45/66/260836aaef14b8254bc449b3163fedec06ef0a0bba0d6a999c918479b2f9/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e75f9429d4533ce275c98bc68bf47fb237ae7b32c954266dabc5edab0c7d682e", size = 501834, upload-time = "2026-06-18T13:36:07.469Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0b/84c1542bf8c465b456f742318ad83eace63551e7f603b06c817b726670af/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f3cca9ca5e2c2dfd7b885f0d34c10b993a070d3593f3cdfef785195da36fb0f", size = 607406, upload-time = "2026-06-18T13:36:08.913Z" }, - { url = "https://files.pythonhosted.org/packages/48/7f/1024c22657a0c0572c4fd5189fad3127cb46731fb26fad3be1e8a4a64972/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1ef8c561fdf88fec205e3d54037824cfe2addce16b509a8d2ecb69daa904cbb7", size = 567623, upload-time = "2026-06-18T13:36:10.14Z" }, - { url = "https://files.pythonhosted.org/packages/15/0e/ad7424a6444e3e108a22781c2e164e82752da5db23ccc5cba8b4470c3164/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e3acb5e1451232381daea01645a98c69de4bb9ad88d77a1f7c1df4d83d54e62", size = 530659, upload-time = "2026-06-18T13:36:11.649Z" }, - { url = "https://files.pythonhosted.org/packages/69/60/cf1666d0dbd6fa869b6de3b85a17254ff0ab10ed286fd59366148bf08e89/uuid_utils-0.16.2-cp314-cp314-win32.whl", hash = "sha256:b5f8e7d0bb2c6e6180176237f92d2e949626e04fcf701c49d73f128e1f64e1d1", size = 169272, upload-time = "2026-06-18T13:36:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5e/111908bdc7287b2589e9a9f10be8e0358844fb4a0554677cbbe0ade49766/uuid_utils-0.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:bf922bad7df257336b594d316a1657df569860bb5389602919001fa6fb17f06e", size = 175435, upload-time = "2026-06-18T13:36:14.114Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5d/b3bd7415622060dd17d587545e3c037f83dc0dffb8880ac798ca7936f630/uuid_utils-0.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:fad82e6482129c58ba9b00da6c247ab6e767645ab17981599229cce19d7b2ce9", size = 173553, upload-time = "2026-06-18T13:36:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/92/43/401acf6fc0e0665dd11a095a28f6d22708c6f8f148c326cfc5b0b1ae9882/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0609e7e906c08386b7f33141254df05dcab24f1c4884150988dc7a287516aca", size = 567548, upload-time = "2026-06-18T13:36:16.848Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2c/cc2bb8273d414d651acafccc3705a8843c130a541fcce65fbeaac22266ba/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9ad2adeb941292fe02e1e5c70b80a5746c45b1b77594506c2a1421455d8384f9", size = 291348, upload-time = "2026-06-18T13:36:18.145Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a8/fdadd7ada0de53dbc03f719da0948cc275abd24d8013a26e42e50d3665c1/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d906c00f965d5c5f4812d0086dc49bf813285ea84c97e8816405200e146f805b", size = 325495, upload-time = "2026-06-18T13:36:19.417Z" }, - { url = "https://files.pythonhosted.org/packages/16/42/e397a1eda06b20dd3a206e3a55b346ff2caad23906586801a87359530864/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a59205fc15463dd0f978f14df14307737e3d4e8ef4aefa29a9d0fa766d84d16b", size = 332301, upload-time = "2026-06-18T13:36:20.747Z" }, - { url = "https://files.pythonhosted.org/packages/46/be/12d3df7bd824e3ce71630c022184a5aecfea92b0a7fa70459542b237777a/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac82500329ffaf2788dac36cf133e1e4e23b6d5e1118274ea6749c3b512f4f1", size = 446760, upload-time = "2026-06-18T13:36:22.198Z" }, - { url = "https://files.pythonhosted.org/packages/f7/10/0c5d1dd6874fa35e2cb66a8499ce303eb8678bef226951182603bd30017d/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8257329f26905f009aed694bd3b17f334f43748b03134dc7bc99d6c5b4e371", size = 325781, upload-time = "2026-06-18T13:36:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/04/e2/9ebb8414875e5c14737fa7145a023458c9b15754f1d129cefe7824197256/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e04b5c10c6fcf9d9801084d1e86c9d7ada7eb48fe07ee4ae5e7fe5b1a852db8a", size = 351189, upload-time = "2026-06-18T13:36:25.09Z" }, - { url = "https://files.pythonhosted.org/packages/1b/5c/168d1f4d30b33c08365debfe4176c2f713a0940f1f11a64128a186d050c6/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d4805c4739dd06d539f8f4fa94f5aaf26eca4b3ece1ef134d4ff904c6b08dcf", size = 501866, upload-time = "2026-06-18T13:36:26.31Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8d/003865d5ed5bf82ece80bd61edb2692985f7548051749fd10f34edb16705/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:76632d2e16e26de777851ec07961ceaea14e65167d0603a0b17fb169fa9ca37b", size = 607632, upload-time = "2026-06-18T13:36:27.704Z" }, - { url = "https://files.pythonhosted.org/packages/ea/52/6102f21f28323b27122a6aa3d4cea183b4fc401868c5c40767e1b9f53beb/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c02f85f49c9c2abbf247a8622458c30232332a28711755aa191da5f38015af6", size = 568216, upload-time = "2026-06-18T13:36:29.377Z" }, - { url = "https://files.pythonhosted.org/packages/68/50/644e4e55f47048d12bc20665fac85bc1fecbed9c892acfb91626abf8ad8d/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f668035ea9faa763e8f1ea42040e8439db88cf2517056d47c348a62a257a1d02", size = 531370, upload-time = "2026-06-18T13:36:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5d/d98d99f601d70cc00287dce5aadef9c199912f0d64343962542f35e7db59/uuid_utils-0.16.2-cp314-cp314t-win32.whl", hash = "sha256:62b8841895eff1c0afbaf5f0050411667231160478c8ff9f411742abffd3b619", size = 169424, upload-time = "2026-06-18T13:36:32.246Z" }, - { url = "https://files.pythonhosted.org/packages/a6/af/c0d482bdd637a8a742d3274cec462b770919f032e179216f2fc2851afaf9/uuid_utils-0.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e9064805881c30dd80a4189a0da7130e3d684de353ea36edd99c1b994bdf429e", size = 175544, upload-time = "2026-06-18T13:36:33.75Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/aff8b0456e8a63672fa89ea9c773f7547a31ff7b596a40f226bf148921a3/uuid_utils-0.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:3324bac95084e63e28553c92fac5a0394c636a76e03e50a7dab0c0bbddf87fa5", size = 173972, upload-time = "2026-06-18T13:36:35.076Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f8/5a/5da7ae85b38e3eddba0be3e8e4328f90882fe92989728e6fb552963d4c42/uuid_utils-0.16.2.tar.gz", hash = "sha256:fa637e4f314ad5b59ff6d8e809d506443d68bef30bfaecdfcfe02cce689abb2f", size = 42962 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/07/294b72a572218bf6e92355203b832b3356c58a7e1e0b92a034497d15bef9/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f064dc54c6abecb09eb104d953bfb079f3c395e0d6b18899979f852d1083549", size = 560726 }, + { url = "https://files.pythonhosted.org/packages/e5/3c/1095b6ab574a7fa69136d47bab5a43f320a8f00a0ecb96059fd49b1747b2/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:dd7aa18db5cc826d482d876a826fee445839701f81f78567e7c74b4458d57a84", size = 288065 }, + { url = "https://files.pythonhosted.org/packages/4c/9d/6404d48fe71def0733c9568d96043b2e1945e2e4205c4eb525db3da42ba3/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc25ad320c9b44c2d3ed33aff4f85b0b277bef4ff79b12c01ee58b52ea44be1d", size = 322946 }, + { url = "https://files.pythonhosted.org/packages/74/00/8a009762015a134aa04b5451400e0ec9832ccd598ed4845f9aecb0be6299/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0ca752d51d1004caff65fccffd44b32a26cb099b546e0512cfa09facb683d6c", size = 330186 }, + { url = "https://files.pythonhosted.org/packages/b7/b0/1613bb98ac11234145aa5bc1de618be536818fef05dec595efb3e2b37097/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8323136bb02355c1b973492ab98b0722206dfdedfb148e4115c35fcdf3889bad", size = 444583 }, + { url = "https://files.pythonhosted.org/packages/93/66/83e62c7a152bbbb8b30ac58eaad81f3860ba2fba91a334c50f223f9ce878/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf8bfdffb22f620635580b17fd178272f30a9841b824b19b935c8db64bf09b6", size = 323064 }, + { url = "https://files.pythonhosted.org/packages/15/37/c1b2faaf3a9d7952f321a9fee3ad74e05b25878bd9b7cd6b0398fe77f279/uuid_utils-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61454f2139424a6cff14eca7849c28b3350f261453b74075aa20fe99592dbb16", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/24/d8/cdf79b242e41ae47b7cd617ac5d48f15ce44e81da8000379c757091ae5f8/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:725110434a1d482a639a9ac467a24f1cb531d84ab52e454a13fe145b10b42cae", size = 499187 }, + { url = "https://files.pythonhosted.org/packages/be/10/978d5ad82bc0fe7ff02d5be6f1eb83b090849f0a95bf8438593565273b7a/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8197870739a3094990743a80f075fa0b17beafd6c187e5f360e021d90a12a6d1", size = 605696 }, + { url = "https://files.pythonhosted.org/packages/3a/28/e382ee44a592e35b80397b493bf3fbbdb8e30a64eaaefc7dabc246aeb253/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e10a02b3a31ed44c7c9a96abde335f5fa222735e73f3081d693414377eb3b016", size = 564975 }, + { url = "https://files.pythonhosted.org/packages/a3/d0/f6011dbe4e5d751a8494715e014019cb5b242d8cd6dbec1cfec3d3fb2e81/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd32dbca0792b9683160151dc07fad11b915020eed7c82b43faf0862c2ff06a0", size = 528462 }, + { url = "https://files.pythonhosted.org/packages/42/7f/279e6159c37f43feb9dd70218b49a26696cefddaef1db7f4b79895eaf5d5/uuid_utils-0.16.2-cp312-cp312-win32.whl", hash = "sha256:dcdfcab60562d12dd43c1a6f495b1d089e41f0e10fac37d94db285d72b678c23", size = 167047 }, + { url = "https://files.pythonhosted.org/packages/47/38/f72f7bed062601448ec2db47351e6c1faccd78fd693bbc6e067299d1fa11/uuid_utils-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:97ee6f5e803ea571f5f6da42efc97d8c5a13f121043680177f8470529b94e855", size = 173821 }, + { url = "https://files.pythonhosted.org/packages/37/61/8a025284a31c85b7c0c5319e96868c2c09dea3fc5f676c979a4cd4baf2e7/uuid_utils-0.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:72cfd9ff1e8a7c371a044687e77eb873721c4a9f4814e453439bfba595b84303", size = 172206 }, + { url = "https://files.pythonhosted.org/packages/ed/a1/3b48859953ee74fc26628ca5d9e5f848209655a0a8c934032fc596035976/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c19b7d595d12923da682ed13d313c2333b9ebf214e65a47a24927a8a3a81b191", size = 560753 }, + { url = "https://files.pythonhosted.org/packages/ba/1c/77635489de5454f2a25411030f78d31931dbdc0c86114da00adb9b91f120/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:909e26fa2451c8db31b9ed1d3c8e4ecf513b6d1619db4205997fe99eb6b4ef4f", size = 288056 }, + { url = "https://files.pythonhosted.org/packages/ad/0e/8e799537ea458abaefb0f5c3b3b05304d3faf413feb0997605a3f8ae2484/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27271b37fbc6812bb1542c4b8e22ee00223a6bf7f62b1f38d3bcf8e92f6d9acd", size = 323196 }, + { url = "https://files.pythonhosted.org/packages/e8/92/4e5b412d4710617fb83ed77b361f5fa6247b99bde2fa6ee07ddf851b59d1/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc4b9d96a2c689d664cf3fc7f7db46b82d2821fb2ce8a4f0798fc0a92c1569f8", size = 330858 }, + { url = "https://files.pythonhosted.org/packages/ae/e3/8173202b7cfcfeb4a588c5f8b85d3e2b44973384eb33167ee25c5c78867f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3bf41b696b0fe808df1b4091c70273a52ea033b0fe97341cd67ecd76d22bb3a", size = 444813 }, + { url = "https://files.pythonhosted.org/packages/37/0d/c3918356932ce467b11e954d0c93697fb4652cf664957e3d9521f7ece22f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc329be41bb6534ecb03e50596179ab76c7643ced33d13c66967d5ae1869663", size = 322828 }, + { url = "https://files.pythonhosted.org/packages/f0/80/4020556682441b62a25b7d07798812115fca97d417a3498d5af6dce36504/uuid_utils-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4125bf6ed3ae443c05e140f8585d174b9d647295b12034d5ec94ae2ae38edefa", size = 347909 }, + { url = "https://files.pythonhosted.org/packages/48/2f/a1e87e268df98f6740af81abf225532c173a971c64df0258c84b630e35a7/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:840b21e609a9b203eee06bdc73e18397154447a9814a8e78d9b68e5104d9802f", size = 499469 }, + { url = "https://files.pythonhosted.org/packages/25/75/5a1f297a09556c27d9617c44ab0510de5f3a70120df236f66b9d0fdd1976/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5119bec75f56bd028d97472f72b1ed723a0d60b09a48017dc70a3cb1892ed081", size = 606160 }, + { url = "https://files.pythonhosted.org/packages/7c/de/140f1d2a161320d1ac9073a03b9eb31fe35ae70f56f8971ec1fb45c14a44/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9fe600ab7d3d4eb56986e814042c917e728ac92cd8a41f099a6b59b84d8bf9e6", size = 564856 }, + { url = "https://files.pythonhosted.org/packages/01/3b/9a5fe6691f8f6d72899cdc2713ffbd845b8c6981eeeab66d98a71b721116/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44020a4532229ccfbba353138539774686350dda71cf4368e257973dd8ba403", size = 528376 }, + { url = "https://files.pythonhosted.org/packages/87/ad/47c93dcabd00f6749803a00be361c75d7079c78ad5e67077dee63d30b687/uuid_utils-0.16.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:280d4f1f22dd2e79c1cc31ffc7fc26dc3534ffc114dedcdd29cc8489c5ce9c98", size = 98033 }, + { url = "https://files.pythonhosted.org/packages/c0/fd/8de85eeb8dd59354ad46e897ab0d0f0fe6bc48702239a6c9f2613f961c8e/uuid_utils-0.16.2-cp313-cp313-win32.whl", hash = "sha256:4942b26ad12c5187bac52b7fb4685040139ff0df9a19cde33e5025326f6180fc", size = 167054 }, + { url = "https://files.pythonhosted.org/packages/86/b3/b5ba393fbe5142eb9d5db23d4b9b16dde2a4e1aee6f2fcb7fadef97e419a/uuid_utils-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:01f81c71cf2185de0707e9d2f248e17025ba50af0acd3cbf51cd8aea96c2e0be", size = 173481 }, + { url = "https://files.pythonhosted.org/packages/b2/79/4e5d63d605b13201ae9af6fcc36ec77949cccc99486c430c016d8f8ed274/uuid_utils-0.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:c1dbe65ce6d46c5f645356d64bfb2de7564e2426ca8c9b1a0a401d6f7ae5cc22", size = 172197 }, + { url = "https://files.pythonhosted.org/packages/89/3a/0e5a0c1e1e3243cf5f12efd2b88a33e63c38b6a79483d3c84b2f5e7265cf/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:617955f4b3f649617c0388127d8a257202189d5cc3c720313f8b207df1cdb2a4", size = 566227 }, + { url = "https://files.pythonhosted.org/packages/28/b3/2b6f9d6832e939aaf2b2ba89ff70b3994cfa3ae9b14daac3329eb9202ef8/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0aa2569908bdb21ccb216cd6bd06cb934351ee65ea7cd5e351e19f633a99b577", size = 290301 }, + { url = "https://files.pythonhosted.org/packages/f5/27/8bb31429884b9f340f964ed70b68bfd81cec61f6e6877633f6a014358e78/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4af7673e84e1ec6029f18d3a0408095c471c4e2691b6e46b4e1f0a2051734ba", size = 325409 }, + { url = "https://files.pythonhosted.org/packages/1f/87/3b59aa97e788ca4fa46e2a3856ef567b51e03fd7fbf27d39ce36e46478b6/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecadf55ed6b8fb72e7966b52fd02919e7d7bb8e7bffeaf285803b82e774debfb", size = 332071 }, + { url = "https://files.pythonhosted.org/packages/1c/21/8c21bf6cf3ce9447b73cee6a38ca63c9bb2f3145259422646bae8e8ddc21/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:026b96b2f1e6b004579e030692d2f6568ccd0b29d40687213c31694abf570c78", size = 447075 }, + { url = "https://files.pythonhosted.org/packages/95/43/77e83019effe1a5ab7169a2d4bf1bd654bebd850b81c8a937b96bd6b5c9c/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273679723e88544dd2de0564ab7f2fddfa2270faf05cabfdf63c275be67ec2a1", size = 325061 }, + { url = "https://files.pythonhosted.org/packages/f3/a6/7bf6e0165dc191c09bc4e8c011de5463d64c5a651ed38ad6698bfc552a52/uuid_utils-0.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5b1a338b92d1eb121e9eaf06ae3db1b9a5cd794ce318a475f6dc6f9e89c3a8", size = 350302 }, + { url = "https://files.pythonhosted.org/packages/45/66/260836aaef14b8254bc449b3163fedec06ef0a0bba0d6a999c918479b2f9/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e75f9429d4533ce275c98bc68bf47fb237ae7b32c954266dabc5edab0c7d682e", size = 501834 }, + { url = "https://files.pythonhosted.org/packages/9d/0b/84c1542bf8c465b456f742318ad83eace63551e7f603b06c817b726670af/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f3cca9ca5e2c2dfd7b885f0d34c10b993a070d3593f3cdfef785195da36fb0f", size = 607406 }, + { url = "https://files.pythonhosted.org/packages/48/7f/1024c22657a0c0572c4fd5189fad3127cb46731fb26fad3be1e8a4a64972/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1ef8c561fdf88fec205e3d54037824cfe2addce16b509a8d2ecb69daa904cbb7", size = 567623 }, + { url = "https://files.pythonhosted.org/packages/15/0e/ad7424a6444e3e108a22781c2e164e82752da5db23ccc5cba8b4470c3164/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e3acb5e1451232381daea01645a98c69de4bb9ad88d77a1f7c1df4d83d54e62", size = 530659 }, + { url = "https://files.pythonhosted.org/packages/69/60/cf1666d0dbd6fa869b6de3b85a17254ff0ab10ed286fd59366148bf08e89/uuid_utils-0.16.2-cp314-cp314-win32.whl", hash = "sha256:b5f8e7d0bb2c6e6180176237f92d2e949626e04fcf701c49d73f128e1f64e1d1", size = 169272 }, + { url = "https://files.pythonhosted.org/packages/fe/5e/111908bdc7287b2589e9a9f10be8e0358844fb4a0554677cbbe0ade49766/uuid_utils-0.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:bf922bad7df257336b594d316a1657df569860bb5389602919001fa6fb17f06e", size = 175435 }, + { url = "https://files.pythonhosted.org/packages/ce/5d/b3bd7415622060dd17d587545e3c037f83dc0dffb8880ac798ca7936f630/uuid_utils-0.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:fad82e6482129c58ba9b00da6c247ab6e767645ab17981599229cce19d7b2ce9", size = 173553 }, + { url = "https://files.pythonhosted.org/packages/92/43/401acf6fc0e0665dd11a095a28f6d22708c6f8f148c326cfc5b0b1ae9882/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0609e7e906c08386b7f33141254df05dcab24f1c4884150988dc7a287516aca", size = 567548 }, + { url = "https://files.pythonhosted.org/packages/4e/2c/cc2bb8273d414d651acafccc3705a8843c130a541fcce65fbeaac22266ba/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9ad2adeb941292fe02e1e5c70b80a5746c45b1b77594506c2a1421455d8384f9", size = 291348 }, + { url = "https://files.pythonhosted.org/packages/4e/a8/fdadd7ada0de53dbc03f719da0948cc275abd24d8013a26e42e50d3665c1/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d906c00f965d5c5f4812d0086dc49bf813285ea84c97e8816405200e146f805b", size = 325495 }, + { url = "https://files.pythonhosted.org/packages/16/42/e397a1eda06b20dd3a206e3a55b346ff2caad23906586801a87359530864/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a59205fc15463dd0f978f14df14307737e3d4e8ef4aefa29a9d0fa766d84d16b", size = 332301 }, + { url = "https://files.pythonhosted.org/packages/46/be/12d3df7bd824e3ce71630c022184a5aecfea92b0a7fa70459542b237777a/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac82500329ffaf2788dac36cf133e1e4e23b6d5e1118274ea6749c3b512f4f1", size = 446760 }, + { url = "https://files.pythonhosted.org/packages/f7/10/0c5d1dd6874fa35e2cb66a8499ce303eb8678bef226951182603bd30017d/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8257329f26905f009aed694bd3b17f334f43748b03134dc7bc99d6c5b4e371", size = 325781 }, + { url = "https://files.pythonhosted.org/packages/04/e2/9ebb8414875e5c14737fa7145a023458c9b15754f1d129cefe7824197256/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e04b5c10c6fcf9d9801084d1e86c9d7ada7eb48fe07ee4ae5e7fe5b1a852db8a", size = 351189 }, + { url = "https://files.pythonhosted.org/packages/1b/5c/168d1f4d30b33c08365debfe4176c2f713a0940f1f11a64128a186d050c6/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d4805c4739dd06d539f8f4fa94f5aaf26eca4b3ece1ef134d4ff904c6b08dcf", size = 501866 }, + { url = "https://files.pythonhosted.org/packages/aa/8d/003865d5ed5bf82ece80bd61edb2692985f7548051749fd10f34edb16705/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:76632d2e16e26de777851ec07961ceaea14e65167d0603a0b17fb169fa9ca37b", size = 607632 }, + { url = "https://files.pythonhosted.org/packages/ea/52/6102f21f28323b27122a6aa3d4cea183b4fc401868c5c40767e1b9f53beb/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c02f85f49c9c2abbf247a8622458c30232332a28711755aa191da5f38015af6", size = 568216 }, + { url = "https://files.pythonhosted.org/packages/68/50/644e4e55f47048d12bc20665fac85bc1fecbed9c892acfb91626abf8ad8d/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f668035ea9faa763e8f1ea42040e8439db88cf2517056d47c348a62a257a1d02", size = 531370 }, + { url = "https://files.pythonhosted.org/packages/6f/5d/d98d99f601d70cc00287dce5aadef9c199912f0d64343962542f35e7db59/uuid_utils-0.16.2-cp314-cp314t-win32.whl", hash = "sha256:62b8841895eff1c0afbaf5f0050411667231160478c8ff9f411742abffd3b619", size = 169424 }, + { url = "https://files.pythonhosted.org/packages/a6/af/c0d482bdd637a8a742d3274cec462b770919f032e179216f2fc2851afaf9/uuid_utils-0.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e9064805881c30dd80a4189a0da7130e3d684de353ea36edd99c1b994bdf429e", size = 175544 }, + { url = "https://files.pythonhosted.org/packages/86/fc/aff8b0456e8a63672fa89ea9c773f7547a31ff7b596a40f226bf148921a3/uuid_utils-0.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:3324bac95084e63e28553c92fac5a0394c636a76e03e50a7dab0c0bbddf87fa5", size = 173972 }, ] [[package]] @@ -4191,9 +4525,9 @@ dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376 }, ] [package.optional-dependencies] @@ -4211,32 +4545,32 @@ standard = [ 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, ] [[package]] @@ -4246,83 +4580,83 @@ 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480 }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718 }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183 }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059 }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108 }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441 }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, ] [[package]] @@ -4332,58 +4666,58 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/98/eb989c3113908e2ef46d940a53695a1ebb4be5a732c4a4f700be8f8d682b/wcmatch-10.2.tar.gz", hash = "sha256:92204839e3e9c945e1e71d7e1e4edeab2601ed50a5c51ff4f3f97ca711eeb738", size = 132499, upload-time = "2026-06-30T00:50:07.198Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/98/eb989c3113908e2ef46d940a53695a1ebb4be5a732c4a4f700be8f8d682b/wcmatch-10.2.tar.gz", hash = "sha256:92204839e3e9c945e1e71d7e1e4edeab2601ed50a5c51ff4f3f97ca711eeb738", size = 132499 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl", hash = "sha256:f1a79e80ccbe296907b7eaf57d8d3bc49eab0b428d35f7d09986b5079b6e4a5d", size = 39742, upload-time = "2026-06-30T00:50:05.927Z" }, + { url = "https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl", hash = "sha256:f1a79e80ccbe296907b7eaf57d8d3bc49eab0b428d35f7d09986b5079b6e4a5d", size = 39742 }, ] [[package]] name = "wcwidth" version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166 }, ] [[package]] name = "websocket-client" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] [[package]] @@ -4395,58 +4729,67 @@ dependencies = [ { name = "httpx" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/72/e14f62362ecee2bae1fb13e7f48e000a39514c883f2b49568294624e0241/wecom_aibot_sdk-1.0.8.tar.gz", hash = "sha256:dd8ac2917e0b23a190a718b2c13f717acd242af487e2bf98a5ca40a117c133e6", size = 64863, upload-time = "2026-06-02T09:43:50.569Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/72/e14f62362ecee2bae1fb13e7f48e000a39514c883f2b49568294624e0241/wecom_aibot_sdk-1.0.8.tar.gz", hash = "sha256:dd8ac2917e0b23a190a718b2c13f717acd242af487e2bf98a5ca40a117c133e6", size = 64863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/82/8e342b64b29c52d9e92fa6f3596d53df5d2dd3dab871493ce0f871b01914/wecom_aibot_sdk-1.0.8-py3-none-any.whl", hash = "sha256:9577742c1b9d76232ece675342871e26c3f5a9d9617c7f860f951e2c6265c184", size = 28069 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/82/8e342b64b29c52d9e92fa6f3596d53df5d2dd3dab871493ce0f871b01914/wecom_aibot_sdk-1.0.8-py3-none-any.whl", hash = "sha256:9577742c1b9d76232ece675342871e26c3f5a9d9617c7f860f951e2c6265c184", size = 28069, upload-time = "2026-06-02T09:43:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, ] [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, ] [[package]] @@ -4456,131 +4799,140 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405 }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315 }, ] [[package]] name = "xmltodict" version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580 }, ] [[package]] name = "xxhash" version = "3.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/ed/07e560876a4458987511461187b285071f53cde49dd5b25cd8c51091522b/xxhash-3.8.0.tar.gz", hash = "sha256:d72b2204f37840b0f16f34192c09b994b97bd25823d723d47a1eddfacf06eb43", size = 86107, upload-time = "2026-06-27T08:17:28.798Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/2e/4b7c3ab28b7a54ac17eae7e02471c49609d6fc5900856a455feeb847a2a3/xxhash-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fc4bd14f873cd0b420f6f1ff5b5cd0dbfeb05b044a11bb9345bcbbf9749636e3", size = 34623, upload-time = "2026-06-27T08:13:16.696Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/09eea3e1bba6a59d64599cb8fba39f2a0872d06e85420eae989a4da61a9d/xxhash-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31904979198e913239cb61b49f5b849696aeb3b03340da815d1491ec74dcc602", size = 32318, upload-time = "2026-06-27T08:13:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/688bbae31e4e2d6d6eb92acbd3837c0e44ff8c7d435e6da922844ff6efda/xxhash-3.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7338ad13f2b273a1ef0ea97b2db0a059fdb3a1a29298bfa145937c0e4152d341", size = 220461, upload-time = "2026-06-27T08:13:19.311Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/71484ce0dab2fa4a475705d1ebc37a17ff02d40e5df6767b3255cc53120e/xxhash-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54e80e803cb34c8a1d278b491e543af40a588d288589c3e6becc991d5328b46b", size = 241110, upload-time = "2026-06-27T08:13:20.844Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f9/1ac88f02e7df7898541490260b21f2b7f7bd2b233038a0cbd3a3b1bffdc2/xxhash-3.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:353953ea18f5c3fbdd13936fb536aacfb47d5bc06eef0919b1a355df61f7cc31", size = 264779, upload-time = "2026-06-27T08:13:22.485Z" }, - { url = "https://files.pythonhosted.org/packages/25/49/7ea1f128d2fe948ed679020f97a0896cdc6c975da5cc69b53a4a9c4a5def/xxhash-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d761f983a315630eff18c2fec7360c6b6946f82748026e779336eb8141ef3eba", size = 242609, upload-time = "2026-06-27T08:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/a0/da/7d237278dfa1c48722c31010c84a328a317b8885429c8cb6ae4a8fa3e3db/xxhash-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3786a9beb9a3b76241cb7db5f5388b460682c12204236389e3221963fc626a6", size = 473472, upload-time = "2026-06-27T08:13:25.877Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5f/980fda82620a07d80026b4df371cbca12fca0fd94d7087c4ec5d898da76f/xxhash-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c94f5a9a775f36cc522fa2a7e8e2cec512e252d2ac056759f753dc68a79ffc", size = 220374, upload-time = "2026-06-27T08:13:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/14/71/efa37bc3e91e1c801972bcef99eab877fcbd17ec10aca16c550ee2951107/xxhash-3.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:55ce59f9af37ac861947b43ea3ce7b294b5de77a1234b558d0f07ffad0197624", size = 310220, upload-time = "2026-06-27T08:13:28.804Z" }, - { url = "https://files.pythonhosted.org/packages/9d/48/19e40320044dc7051e8446505f18557d5661853b87a8770ad399325bb3c8/xxhash-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3afa1422a32c7c8e79ad5121dc21eaa5cee9e9e67bffca3f15d15d220d371908", size = 238100, upload-time = "2026-06-27T08:13:30.378Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0d/588499f4d7cd064864ada7adfb9e8785f88a988f1332ed4c1be73d249c15/xxhash-3.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:551fda694938be910529452a89175137c58b4739e41fadff3c047e24b1d74a3b", size = 268937, upload-time = "2026-06-27T08:13:31.867Z" }, - { url = "https://files.pythonhosted.org/packages/54/18/fb2ad593572a33d1b6864b33047b8ca7269273a3c56107b5fd33e0b9c8fb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512eb937c9457e6057e230e005c4709dd2ab63a5989f854d69f31db905750a62", size = 224910, upload-time = "2026-06-27T08:13:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/63/9e/b880f9ed61b73492e24bb962d76aeb63f18ccb895f0edfb52e20d45ed6f2/xxhash-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4931ea93840f750a908efebaf23c71004feacc1a4649ef601b96d400a505c9a9", size = 240742, upload-time = "2026-06-27T08:13:35.237Z" }, - { url = "https://files.pythonhosted.org/packages/3f/89/fc682f93e54e486fc338b26a7d6d0d5cb0ab366269273c2608ac62b51afb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2fd4b60e8d9fc3923f39079f185b3425e6d76636fcb66d82a33dd7eba7c30f2f", size = 300527, upload-time = "2026-06-27T08:13:36.997Z" }, - { url = "https://files.pythonhosted.org/packages/80/71/a4b4122afb2d17ad69e0922cfeddb5ad5c25b02f37eed3dd3819d42e5f55/xxhash-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1da00075f1605794298878cb587f7533329693e2a0c45bbd25d6353644add675", size = 443195, upload-time = "2026-06-27T08:13:38.719Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e5/ed3930f5dc90f4b1bab5ac3be099e8b2e81c1262d85e4adb5f2758e30d23/xxhash-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba73801c87d44fa37b2a5feab3004f0a654506027bf032ceb154d94bb74ea772", size = 217252, upload-time = "2026-06-27T08:13:41.179Z" }, - { url = "https://files.pythonhosted.org/packages/44/ae/128ea5794387ca54bb4084566db20dbdfc9c21cb17b67d3fcb403927b5ba/xxhash-3.8.0-cp312-cp312-win32.whl", hash = "sha256:0b0836dee6022e22ba516ebfa8f76c6e4bda08d6c166c553e40867bac89e4a54", size = 31890, upload-time = "2026-06-27T08:13:42.568Z" }, - { url = "https://files.pythonhosted.org/packages/4f/04/a6c182dc566c88e8d1a497d22cc4ffdcfcc0a9fa80325efa6cd4b9002c54/xxhash-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3bc2a09b98b8f85c75208cd2b2d2aecf40c77ecb2d72f6bf9757db51a98d3499", size = 32677, upload-time = "2026-06-27T08:13:43.705Z" }, - { url = "https://files.pythonhosted.org/packages/93/b5/aeda4e79f962c8d58ec60cb20a5abfe91c9f7d62e626f69f6659bc0bd0c4/xxhash-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:208e6a8b93426896d803224e9fabe26f8b9c651e8381a80b1fa31812faa091e3", size = 29155, upload-time = "2026-06-27T08:13:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1f/96f43c5c7c7c4d44721f8d2e5d74698c667a30283c4b10a7e50a56804ee3/xxhash-3.8.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:36434c1d1b0a4729df1fa26ab11bffed1ba52666c0beb605c98a995b470cd143", size = 38508, upload-time = "2026-06-27T08:13:46.152Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d9/7d5d6af4876c6481f2e0acb2dda64dd5209574bf7ba1ad4f6af7a1f8d473/xxhash-3.8.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:a5e6497cefcb2d67f1745c66df9718a99112583af6cc2b70da0312a2eb939f1e", size = 36542, upload-time = "2026-06-27T08:13:47.497Z" }, - { url = "https://files.pythonhosted.org/packages/32/ff/66fed439d78c5a09a1491a85af29bf8923b516530116731a9ac6b14dee2b/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5b00b82f1be708da9404fefd658cf5cf3be5ee3be2aae4bfe3b874255badd342", size = 31102, upload-time = "2026-06-27T08:13:48.721Z" }, - { url = "https://files.pythonhosted.org/packages/56/b8/9fae0399281095f8aca1f32b21947b3c3c75ad6021b255c5c6e4b11d3866/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38b0cb0ab7f283413b7cace2bf710d7cf8f702ea82cbc683908691d52028a89b", size = 32096, upload-time = "2026-06-27T08:13:50.138Z" }, - { url = "https://files.pythonhosted.org/packages/61/a4/e53d162c74a8a2950dc063969914387b0680da4c7c20ad17744ec03a3b0a/xxhash-3.8.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:084312171a9798dea85e924b2674f5e1a44933050a1ea1cb1c6b1364e004c66c", size = 34585, upload-time = "2026-06-27T08:13:51.572Z" }, - { url = "https://files.pythonhosted.org/packages/69/f5/e12397e3f2c4917b6572e103a3277cd27cc56330e304bba61d195d7e5224/xxhash-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a1a9e845bd3bbc57d9356819e0d198fe23282e0576b398a6282a0f8fdc75aef", size = 34622, upload-time = "2026-06-27T08:13:52.818Z" }, - { url = "https://files.pythonhosted.org/packages/70/80/c053dc51af5c942229689a0e9cb66fdc999bbd840f645e761f5ab73cbb17/xxhash-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ffbde09743ebaf8957b8426948fbe85eab5e5de0d29eec407fcff5a2812a3cc", size = 32320, upload-time = "2026-06-27T08:13:54.04Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/294171b67dfe770e1293edcf2a3f7e41302cdb8aefb258585312191b3ffe/xxhash-3.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a6dee3952c2b6e82e7f1dbc5dbc6167f9c84126851def7926e32827c2816169c", size = 220532, upload-time = "2026-06-27T08:13:55.448Z" }, - { url = "https://files.pythonhosted.org/packages/80/c3/d141bfdeca785c8c680abf867d4b52a5e64a55d90df242c3141a3e58c4b2/xxhash-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf8ff8e12416c9fa05b43c7509b9332d6ffc4090413c4e7a1dee8599763b6d59", size = 241215, upload-time = "2026-06-27T08:13:57.047Z" }, - { url = "https://files.pythonhosted.org/packages/09/5a/aeaf35143a6f3d44db73298e861405bdd9c9dacaedfc369cb43d9fd65282/xxhash-3.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cebbb322df4d97d8ef2704f49ed2f6f21f6702fafa0dc0c2a6ae70e904205689", size = 264615, upload-time = "2026-06-27T08:13:58.912Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/f8ca782bb34f99693faab70a7989bcc84f62ffe93c9a4cca464a33507a4b/xxhash-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a8d08707b4100ebce598fc59fadf04b42d79b855818d6994f8f0fffd1df8edb", size = 242682, upload-time = "2026-06-27T08:14:00.483Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/ddbee4ff1542c2e88e72269a5a6bd18c3f26a80c2514e0918f5d1f3e9ec5/xxhash-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cf5427602dda15d8ce3c6d870d29bf07d43975f59c9d6d3f7f6f93a901b28b12", size = 473551, upload-time = "2026-06-27T08:14:02.17Z" }, - { url = "https://files.pythonhosted.org/packages/25/f5/a680d48dddab37ab2fd9189ca03f775e29e3627122e30790816d7eb365af/xxhash-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97d7bd715ea5050b6c9638b52c62adf3055b648ef6eee6892a4cd9697b530191", size = 220485, upload-time = "2026-06-27T08:14:03.765Z" }, - { url = "https://files.pythonhosted.org/packages/22/b1/7ac129b74981c07f1ff9c649f204465e86f83f9f29b2ebdc70d91514c365/xxhash-3.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cd25bbbab37d898f6e5a90905ce6ae2c1f8bd6668c07cef406fb3e8c8c570dd", size = 310307, upload-time = "2026-06-27T08:14:05.366Z" }, - { url = "https://files.pythonhosted.org/packages/67/e6/43e673411249dd63f6cd974523a1b32fad75cf5453e363bc8f44af215fb9/xxhash-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3e30e5c057f483c3c53a11b53eba091a737cb19dfead36c8b23bf5beb4a169cd", size = 238164, upload-time = "2026-06-27T08:14:07.149Z" }, - { url = "https://files.pythonhosted.org/packages/e5/95/87f8baf41f63130f3637104b7a610f82b20106332fc6e289c8dbf7955d0e/xxhash-3.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:07dd44d992ebd456752bc25b1c42cd172d94bd8cb24049300449ad0716081c3a", size = 269062, upload-time = "2026-06-27T08:14:08.834Z" }, - { url = "https://files.pythonhosted.org/packages/38/c9/3369b497cd1f926b930c52fd2400606f177790d887b49f9e86bddcc24562/xxhash-3.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3118600a3102d4707dc1c485dbc3acbbbf37819069ad3e7854e77b923745d76b", size = 225007, upload-time = "2026-06-27T08:14:10.689Z" }, - { url = "https://files.pythonhosted.org/packages/34/c8/03dceb86a8128858ac105bd6e282d62b3db6fd421a79bd8a9f6b8cdc47a7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ed37b0c95d8fb3fbaad5e13cc0a9727eb8739d1d54b2adef28108c250cada3a", size = 240815, upload-time = "2026-06-27T08:14:12.195Z" }, - { url = "https://files.pythonhosted.org/packages/47/a5/ebd43eeb1af1dd8f0201943688b20958e99d3f6eb36481fb8c37b55ef139/xxhash-3.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bb043da412e478e7b1db3407051124b85b133803794d3809ad6d92870b304fc7", size = 300632, upload-time = "2026-06-27T08:14:13.916Z" }, - { url = "https://files.pythonhosted.org/packages/df/24/c873e41a3c00dacc385c8ff08c007723f6a528922c1cea7fd9684e86dae7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:196fc132683d9311a0bdce8388ee52bfa07fdc1987cc428a27956e47ccd7b50d", size = 443293, upload-time = "2026-06-27T08:14:15.446Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1b/c671272fe28f70574e3c574d58465f26460154bcc68876121872afa1c14d/xxhash-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfb5411af3b77c75e99db100aa15c5ba623c85d72c565e4d7a0ed1a986ff766e", size = 217327, upload-time = "2026-06-27T08:14:17.28Z" }, - { url = "https://files.pythonhosted.org/packages/57/43/b45a52f795812cb769b6ac159e69b605d18b1c067749e63dcac159e90064/xxhash-3.8.0-cp313-cp313-win32.whl", hash = "sha256:6d1d6179e26830c6690fac63f76d372f69714b977e12ca9c42188a60f51c59f5", size = 31898, upload-time = "2026-06-27T08:14:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/a1/42/2bd70e4eec25dc5990652979d708d4d7c999793d7d5af5d0e48ab4374dc1/xxhash-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7c92427a56a12f4d5c7bb26dbb9e9a4658c313ecb6c2f1dca349902e3822df07", size = 32680, upload-time = "2026-06-27T08:14:20.277Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c8/2fe61edb6144183cf094035a8c5354c65a073127acf6379655ed1e705b70/xxhash-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:9fc8453642c1c6d38b4fbac8901c2452ce1fa88b27f003bfee6703cbfae9bd63", size = 29157, upload-time = "2026-06-27T08:14:21.674Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b8/81d17a993b9a4750ba426ce966421681bb4b8e82a460cd346756491b8cc2/xxhash-3.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:efcacb644a915f010dc477447b045e5dcde1afaa40d16b2f0f8e7cd99c9e1635", size = 34897, upload-time = "2026-06-27T08:14:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/f5a368e3273440b3ea58fbd3f0b08c19f552b25ca59f43f5732ca96d2126/xxhash-3.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1e0dbc510cff94c5efbcc2b82c28b41519fad09b5b1f9f3d99c63e3940e49a0", size = 32630, upload-time = "2026-06-27T08:14:24.603Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ab/f424359c91c55f564fbbe4e454a126eb522471109f67376f20ad19c5e663/xxhash-3.8.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ff19d016a41c90d1f519005887191896b6da1274e1d5d48b347e17eb798ffc5a", size = 225874, upload-time = "2026-06-27T08:14:25.992Z" }, - { url = "https://files.pythonhosted.org/packages/ac/c2/434579ef9235123b6c9bfa89c5614e0001e988613b91557b24aa326d9faa/xxhash-3.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aafc3eab99c50508852e34307e9565933bf128cad084cac7d2471b7ab1743de0", size = 249705, upload-time = "2026-06-27T08:14:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6c/3c0c917331ca3c71f826cedce2127f230624e2b49b992472dd5e9e72101c/xxhash-3.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e521368ed79ae6c4d31e1e417726643c49d7d6e286f4fdabf9a8330ed8a8ff7", size = 274716, upload-time = "2026-06-27T08:14:29.495Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f3/a8bb98d3307c67e88be9642dff52854c3de3f488f95989b60ff69c8dcc42/xxhash-3.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a0127688d116ec0c225e7e1f744e3f206de2b8822ffeb31a9ab5cc6384f92c5", size = 252019, upload-time = "2026-06-27T08:14:31.247Z" }, - { url = "https://files.pythonhosted.org/packages/f7/73/fab69a2e5b6353dde643209fe9b6adf4fbd64c888e531deffc476bfb2635/xxhash-3.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:22c0b17da2f9fea0f8836538512249871b359141616bad44c58d238b5f011f40", size = 482024, upload-time = "2026-06-27T08:14:32.973Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/ba34099b5278097ec9c68c0b740719813553bfd11ca17e7353de6d2a41e3/xxhash-3.8.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d49465646b1a5e3b1729c5f636e05676a2fb52e203e3b22a5411c416c4c5302", size = 226655, upload-time = "2026-06-27T08:14:34.608Z" }, - { url = "https://files.pythonhosted.org/packages/76/0c/90aba4708a37fe752b324a7cbf10058eaa33e892cdd62751ff17a5137b93/xxhash-3.8.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2853dea1e30ed00ca87dd87d76da5da063d302b823b3fb80ccd18421de0f251", size = 319583, upload-time = "2026-06-27T08:14:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/38/46/42e349e2d3017b2688f4cb301742c37c438e77963e3fef711edce2fc5c65/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:82f0102a2a3760287b7cd7f9e0a30edd4c3b18762ed1a242208d43c8e2bcf30b", size = 246000, upload-time = "2026-06-27T08:14:38.104Z" }, - { url = "https://files.pythonhosted.org/packages/ee/15/741b947ae3c768e82018c46846f8616f6aa9b5042649f318a1a6897defe3/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:b8414a66a7524596d841cad5dc1adab6ce76848db5ab2b83db911fbdab1417af", size = 275455, upload-time = "2026-06-27T08:14:39.841Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b4/a9db84c9458fc8f53eaf0051377d1e9eecd9f330fb1225640027417a309d/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dbaa73df10414ea1e41b98691a9d8241d4c47ad8d02c726587a3cda05278e53", size = 231209, upload-time = "2026-06-27T08:14:41.543Z" }, - { url = "https://files.pythonhosted.org/packages/20/92/60a868cd34851746d0b0d95dced0f42867c7c00606f6e5dba85b70b232ce/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:43fc9aaba10ab4267c90793601f60d35c3c9caa1544eceb483618a71ad9ce7da", size = 250416, upload-time = "2026-06-27T08:14:43.193Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6a/168ca46a4679c32aae9246caa1fddf35981d6304487e45e992b3d4530324/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ec5eb3d28fbb9802c6d2526f772133a06c91d6f03756fcc67c834b642ffdd51d", size = 309764, upload-time = "2026-06-27T08:14:44.79Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/13646b348c07679c818791ab2d35415db5cb20f3bc77daaa255909a401b4/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2b77c301b644cd9b4d0749a3291081ec2048a6bef7fe0487c993bbba3efb9ce0", size = 448650, upload-time = "2026-06-27T08:14:46.562Z" }, - { url = "https://files.pythonhosted.org/packages/59/9a/3d244b2acf6bbd86a363817ee09084b4684e8e11840663e19869e9e0d952/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d7ece11a132325353890a144c30119073617a1299c593ca29b96c315b07e1edd", size = 223572, upload-time = "2026-06-27T08:14:48.294Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c7/143410d026a6e0d86dc69037ec2a3b8db810a54e7f443b340ac17612be2e/xxhash-3.8.0-cp313-cp313t-win32.whl", hash = "sha256:b21db84df7b9d54d9e4195a964243c1b32d745c6fbc0cfcfffee1d4bd297196a", size = 32301, upload-time = "2026-06-27T08:14:49.687Z" }, - { url = "https://files.pythonhosted.org/packages/6c/db/2240b0638161637b2f310231748a7a6a06c79fb43a3adb34c96f359762bf/xxhash-3.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0643b7d9f598f6da6f1f6b899f4358250d0fb853242e2d712cbde27bf5a99d29", size = 33221, upload-time = "2026-06-27T08:14:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d8/52038e4fa5baf4f00654a225516168d02908edfec7ca104fbefc58af394f/xxhash-3.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4bbacf2e938526969f8ab3334d4ac3da14ea059e1dfd1339a92f9091467e750f", size = 29294, upload-time = "2026-06-27T08:14:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ef/a09907aa28bdcdf6810d5c26656b154c60c0f06bb8db8442a1192d9c227a/xxhash-3.8.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:557e2a7cc0b6a634cf9c8e5c975d96b7da796fdeb1824569d760cf0f25b6f33f", size = 38365, upload-time = "2026-06-27T08:14:54.166Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4d/d991ff77bc489c2231025e64e570502156d573c7bff69c917589cc307089/xxhash-3.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:dad744d1613cbfddb844dad93adbffbd51c3e9f53ceea9568f7c3b94bedc19a4", size = 36477, upload-time = "2026-06-27T08:14:55.427Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0e/553eab001f1e274da73da074968cdc8be8cacfb318937ab9871b8e1909cb/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:953f29b22c04b123cf3cd2e08bccde3a73184aeda5a1038e0054cb3355644120", size = 31116, upload-time = "2026-06-27T08:14:56.897Z" }, - { url = "https://files.pythonhosted.org/packages/55/d5/d0f4dbe7b4d9ce0125f16e45ec0be5e04f6a172edb4e2fa551c4f2eb5d7a/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:aa699e0253ceffecf41cae858d0a11f2439d6874a0890b556387bffe11dc1c08", size = 32112, upload-time = "2026-06-27T08:14:58.126Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2f/b332c7bede6a676343f2c9c8dea233c8c82753eaeda6f7a2c321d8c58ca3/xxhash-3.8.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e232c82466babc13e956d53aa84d0149660ed6886bc195248bb4d03bf2eca301", size = 34618, upload-time = "2026-06-27T08:14:59.458Z" }, - { url = "https://files.pythonhosted.org/packages/b3/5b/2bf3c9e61c7cf8f53bce937af45e22b72bb1f224d5afb20352beba0d628d/xxhash-3.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f75fd1c6a5028f345cd4a8c52f4774d2e5b7809fa58111c60a5502b528914a4", size = 34739, upload-time = "2026-06-27T08:15:00.863Z" }, - { url = "https://files.pythonhosted.org/packages/64/b6/e88521f5736c181b89bfb7ab756f0ca658a8a1ecece7277b75e167717614/xxhash-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b49d7e09b211a1ad658dbe2dbf6561eb92f2e6926bd1101e2d023178371f2d6f", size = 32332, upload-time = "2026-06-27T08:15:02.383Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a2/fba440739fa5f86d2c28738c202e88d3dd063290c8bbb20e183c5334456a/xxhash-3.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ceb702bc8e56b7f1f1413d42aa294045b9a0e4c9888e07edc5cd153e8c4c948f", size = 220479, upload-time = "2026-06-27T08:15:03.785Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1c/4a1639efec16416695d6c7bc6b224d3f607e0b8cbe2409fa81081a849d1c/xxhash-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f3c96e06bdb122e8cc84f5c7088579f3102b828efd62e9dc964a9d17c7b89e", size = 241409, upload-time = "2026-06-27T08:15:05.439Z" }, - { url = "https://files.pythonhosted.org/packages/92/d1/8ce471f8d6752384f972fd5f6363f2e8d8b867a89fbd724c6dbd91d2bb98/xxhash-3.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:415a8d06ac9bea36b1e06b603a347e0f62401042a97d7bfccec8ae2da12ad784", size = 264433, upload-time = "2026-06-27T08:15:07.027Z" }, - { url = "https://files.pythonhosted.org/packages/95/77/400a281683fd39c54e2ac497fa67bdf886baaadb8c0ba58f7e1ea1d7692e/xxhash-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f5ccdd2deb5dce31201cc0eec94388cce97e681429073db50903fab0a0a8a0d", size = 242835, upload-time = "2026-06-27T08:15:08.703Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a6/edda651cfa0ba8e921791e93468fae655b63894d89730fcbfe46704f0d0a/xxhash-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a6cf81bc699d3a5ebfcf2fdb2a7bd2e096708d7de193f6f322944a02ba00953", size = 473800, upload-time = "2026-06-27T08:15:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/dd/da/50f764ec6a93d3961fce294567e41bfca0e66d168deed354a3dc90ebeba6/xxhash-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4d12a04d7ffc0359f0eadc4535a53cab113044c8d2f262c7e9a56950a5ed50e", size = 220677, upload-time = "2026-06-27T08:15:12.622Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/9fe4ed5aac6f38629cc83b34f84748b83ad8295a578ec6a49d8bf896cafb/xxhash-3.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d209373fcb66138c652cf843385ee60866e50158a7869bbbf8b322d9a822b765", size = 310385, upload-time = "2026-06-27T08:15:14.384Z" }, - { url = "https://files.pythonhosted.org/packages/83/f5/1147e03c0553ed22bbae9ce47503c37ee0c5f95592aae10f339c25f61de9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b88a3fe28277811e599efa6e1c96abce8a77d60dd79c94da7a9b5c377c172b7b", size = 238330, upload-time = "2026-06-27T08:15:16.201Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d8/92daf66c1966c84da5c97a06ced1480208d3a3bd465cb0630565ec00d1b9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5d5a888a5ef997cb35f1aad346eb861cd87ecfe24f5e25d5aa4c9fd1bd3950c2", size = 268667, upload-time = "2026-06-27T08:15:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c0/080c1a92972667e183c04b03f33c877f8ec61cfa3570e61731077286648d/xxhash-3.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:de2836e0329c01555957a603dcd113c337c577081153d691c12a51c5be3282b0", size = 224934, upload-time = "2026-06-27T08:15:19.972Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d5/cbc4e5b2bee10c94cba05b5bb2b8033e7ef44ae742583fdafcd9188e33ed/xxhash-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4bc74eedb0dd5827b3be748bacf9fdb50004037a3e16c7ddb5defae2682cef71", size = 240870, upload-time = "2026-06-27T08:15:22.04Z" }, - { url = "https://files.pythonhosted.org/packages/76/f7/09679b00e192b741b65c230440c4f7e6df3251a9ad427a518ddf262ec71a/xxhash-3.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c571b03d59e339b010dc84f15a6f1cff80212f3a3116c2a71e2303c95065b1f6", size = 300683, upload-time = "2026-06-27T08:15:23.647Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1b/f43ec36e8c6a20c77be0bcca23f0b133ed8a0312681500d1676eebd71924/xxhash-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:87626acdd6e2d762c588a4ffe94258c5ef34fb6049a4a3b25019bdb7f9267a9b", size = 443407, upload-time = "2026-06-27T08:15:25.504Z" }, - { url = "https://files.pythonhosted.org/packages/45/2e/a3e3a779c5e4789daf975e05cc1c7f11bae724a03855120029d4592c8e63/xxhash-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:076d8a4fb290af952826922aa42a46bfc64caa31662ce4e2925a445d0e6ce57f", size = 217559, upload-time = "2026-06-27T08:15:27.234Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/1c1e078ac290afff304a541a2a60965beb369ad65b4f30ec93ea1e0b7210/xxhash-3.8.0-cp314-cp314-win32.whl", hash = "sha256:52f8c7c9833d947e60df830671f6eca810d7c667051243985a561c79f1a3d545", size = 32602, upload-time = "2026-06-27T08:15:28.809Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7a/d455cb83d5e3c94046234294fb5dbbe5da600d1bbdf76b9527756920cce9/xxhash-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:4fbfcb7dd307e23189a71050f6e27746926590330f37d5fd2ffcb8ea78de1f42", size = 33393, upload-time = "2026-06-27T08:15:30.166Z" }, - { url = "https://files.pythonhosted.org/packages/89/8f/1b14471f617bc96edbb9566099a162d918a981381c398114726cc600b76c/xxhash-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:ecef1e65b4715c7326002073763fe94cc44c756a0698508abb915ab3d6be6e3d", size = 30007, upload-time = "2026-06-27T08:15:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/51ad2f9f784121c8057ef1ba36362f58d4595cbcad16322941f5b73eb53d/xxhash-3.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:02ed856a765cb6e006168595d9455ac8c3c4d60cc04cd47a158a1ac677d68f0f", size = 34957, upload-time = "2026-06-27T08:15:33.292Z" }, - { url = "https://files.pythonhosted.org/packages/1b/14/175c573ae4fac48bf21a82e5b9ceec75d64c520c51ca08de3105de539438/xxhash-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eec30461a7b457611098ba7ab09363e36c8b2645b4687fb6f3d405bb646e3410", size = 32635, upload-time = "2026-06-27T08:15:34.766Z" }, - { url = "https://files.pythonhosted.org/packages/96/08/f83efabd350a50c31c851b88891e318a6f07bdbf40a43d0f7bb6cedade7f/xxhash-3.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b471744912d1ce5dd6d3975b7525e77518359ebf3aa1bd7d501e199f5ae488ea", size = 225969, upload-time = "2026-06-27T08:15:36.35Z" }, - { url = "https://files.pythonhosted.org/packages/7c/78/2b6d12da9cf572c84d93b88ecbf9bf6539a7c5219bde128b214396b97c8b/xxhash-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3748d71202bf3f279e77cb8b273b6d0f29d1bcaefb6ce6cb03b95f358863ba37", size = 249851, upload-time = "2026-06-27T08:15:38.087Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/755eeb1882634983b24e6375a95ed233228dc48f0ef12655388bf3c7eeaf/xxhash-3.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b3bf59ea94b2a23b0f992769804ab9401d5cdcd9df0062fe2cd78a491ae8851", size = 274842, upload-time = "2026-06-27T08:15:39.808Z" }, - { url = "https://files.pythonhosted.org/packages/77/f2/09b1231cad17c314e51664c4a004c919108ec59aba10f9a28fa061e7b8be/xxhash-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:40f061aa5379eba249e9367b179515571e632be6d1b6f55ac139e6fe3d08463c", size = 252218, upload-time = "2026-06-27T08:15:42.105Z" }, - { url = "https://files.pythonhosted.org/packages/b2/24/de756d55547953494eb6775aea92e258035647b3ecb8547618cd549001e1/xxhash-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:680d70896a61fc920cc717a0a8fe8a9fb5858c563184666e31874caa54a16d9e", size = 482135, upload-time = "2026-06-27T08:15:44.476Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/b8147633e32f98ef2b4bb0dfca82f0f63e2b02ff179f20664af64c4216a7/xxhash-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14973fbdee136588e57447401b521f466a42faca41eecdf35123c73103512ca8", size = 226776, upload-time = "2026-06-27T08:15:46.597Z" }, - { url = "https://files.pythonhosted.org/packages/29/37/ba051d8f0380d3cf845b23ba058a17d32025846463eb6bf885887fc8effe/xxhash-3.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:96c6bca2486cdc58b125966817a92a6abe6ef1fab86b2f8798a7e93488782540", size = 319738, upload-time = "2026-06-27T08:15:48.394Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/36e0a27dd27ffa3f7b521650cbcd52a00fb86b71343ffadb642374e8263c/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b1109ae238e932d8482f9cb568b56a405cc73bc7a36b837844087f1298dd218", size = 246136, upload-time = "2026-06-27T08:15:50.981Z" }, - { url = "https://files.pythonhosted.org/packages/fe/73/2663dbf4c09386a9dcc8a94d7a14b4609ed4bad8180ced5b848e60a9b660/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1da5db0863400eade7c5a31969754d1392189f26b4105f6631da2c6c7ea3bccc", size = 275568, upload-time = "2026-06-27T08:15:52.735Z" }, - { url = "https://files.pythonhosted.org/packages/d6/58/f3ce1bc3bb3971191f6521273ddae98d3c610bcefbbed5327c3b3627c12f/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c61b5a0f21ace5e886f177cce43826d85a7c84e35a9e17cb6d1b4ac0b7a7d833", size = 231314, upload-time = "2026-06-27T08:15:54.73Z" }, - { url = "https://files.pythonhosted.org/packages/4d/51/835706a36cdc00e5b638fba9b22218b3d40d23a7677c923feca8a3f55b98/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1db4f27835a450c7e729bc9330c6e702113711cea1f873d646e3a31fe96a9732", size = 250521, upload-time = "2026-06-27T08:15:56.853Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/b0b62caa3caee58ab9de8969f66aef1c3729886f3ff60e173fda3f2762be/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4788a470f946df34383abc6cd345088c13f897a5ee580c4cdd12b1d32ad218ef", size = 309926, upload-time = "2026-06-27T08:15:58.704Z" }, - { url = "https://files.pythonhosted.org/packages/69/c4/60e6d18a0e131c7af622374af9deede15d3c47d8e5e7221933481b57b319/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3b6dfa83096cb1e54d082acebaf67f0c42667c56dc48ba536a76cac08d46391e", size = 448812, upload-time = "2026-06-27T08:16:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/12/9f/c9627daa052be39a932d0e17c6bf6a9041d2cde3afacbded9196acf70261/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:57ec0ba5299a9a7df376063c139f5826ff0c89b438703939af3d252c31ca96a4", size = 223639, upload-time = "2026-06-27T08:16:02.784Z" }, - { url = "https://files.pythonhosted.org/packages/a9/38/92916e008a84c1f1a9aef82e4363cdc478a722ff69e59c6afbf93d3d1fda/xxhash-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:d9a61f23b999baeb84102aba767b1b3e94958eab94e6c11b08927e7dc4200795", size = 33078, upload-time = "2026-06-27T08:16:04.639Z" }, - { url = "https://files.pythonhosted.org/packages/31/7c/e413bc75121d9628bf023b2ed251411ca3a447cf00cd9aa3438ab17f6c67/xxhash-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:61069b260fff84116235bb93845f319284dc6b42527c215af59264f4c2ee3468", size = 33953, upload-time = "2026-06-27T08:16:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/f6/eb/21a96e218375bd8b6ecd6d07cf60c8ff1a046e93cdedc3cf7bc3309edf7b/xxhash-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:73cecd431b4f572d38fcf1a7fe85b30eb987778ef9e7a70bc9ffcf2d64810e6f", size = 30164, upload-time = "2026-06-27T08:16:08.009Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/78/ed/07e560876a4458987511461187b285071f53cde49dd5b25cd8c51091522b/xxhash-3.8.0.tar.gz", hash = "sha256:d72b2204f37840b0f16f34192c09b994b97bd25823d723d47a1eddfacf06eb43", size = 86107 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/2e/4b7c3ab28b7a54ac17eae7e02471c49609d6fc5900856a455feeb847a2a3/xxhash-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fc4bd14f873cd0b420f6f1ff5b5cd0dbfeb05b044a11bb9345bcbbf9749636e3", size = 34623 }, + { url = "https://files.pythonhosted.org/packages/43/e4/09eea3e1bba6a59d64599cb8fba39f2a0872d06e85420eae989a4da61a9d/xxhash-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31904979198e913239cb61b49f5b849696aeb3b03340da815d1491ec74dcc602", size = 32318 }, + { url = "https://files.pythonhosted.org/packages/01/59/688bbae31e4e2d6d6eb92acbd3837c0e44ff8c7d435e6da922844ff6efda/xxhash-3.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7338ad13f2b273a1ef0ea97b2db0a059fdb3a1a29298bfa145937c0e4152d341", size = 220461 }, + { url = "https://files.pythonhosted.org/packages/2d/de/71484ce0dab2fa4a475705d1ebc37a17ff02d40e5df6767b3255cc53120e/xxhash-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54e80e803cb34c8a1d278b491e543af40a588d288589c3e6becc991d5328b46b", size = 241110 }, + { url = "https://files.pythonhosted.org/packages/bf/f9/1ac88f02e7df7898541490260b21f2b7f7bd2b233038a0cbd3a3b1bffdc2/xxhash-3.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:353953ea18f5c3fbdd13936fb536aacfb47d5bc06eef0919b1a355df61f7cc31", size = 264779 }, + { url = "https://files.pythonhosted.org/packages/25/49/7ea1f128d2fe948ed679020f97a0896cdc6c975da5cc69b53a4a9c4a5def/xxhash-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d761f983a315630eff18c2fec7360c6b6946f82748026e779336eb8141ef3eba", size = 242609 }, + { url = "https://files.pythonhosted.org/packages/a0/da/7d237278dfa1c48722c31010c84a328a317b8885429c8cb6ae4a8fa3e3db/xxhash-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3786a9beb9a3b76241cb7db5f5388b460682c12204236389e3221963fc626a6", size = 473472 }, + { url = "https://files.pythonhosted.org/packages/9b/5f/980fda82620a07d80026b4df371cbca12fca0fd94d7087c4ec5d898da76f/xxhash-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c94f5a9a775f36cc522fa2a7e8e2cec512e252d2ac056759f753dc68a79ffc", size = 220374 }, + { url = "https://files.pythonhosted.org/packages/14/71/efa37bc3e91e1c801972bcef99eab877fcbd17ec10aca16c550ee2951107/xxhash-3.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:55ce59f9af37ac861947b43ea3ce7b294b5de77a1234b558d0f07ffad0197624", size = 310220 }, + { url = "https://files.pythonhosted.org/packages/9d/48/19e40320044dc7051e8446505f18557d5661853b87a8770ad399325bb3c8/xxhash-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3afa1422a32c7c8e79ad5121dc21eaa5cee9e9e67bffca3f15d15d220d371908", size = 238100 }, + { url = "https://files.pythonhosted.org/packages/d3/0d/588499f4d7cd064864ada7adfb9e8785f88a988f1332ed4c1be73d249c15/xxhash-3.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:551fda694938be910529452a89175137c58b4739e41fadff3c047e24b1d74a3b", size = 268937 }, + { url = "https://files.pythonhosted.org/packages/54/18/fb2ad593572a33d1b6864b33047b8ca7269273a3c56107b5fd33e0b9c8fb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512eb937c9457e6057e230e005c4709dd2ab63a5989f854d69f31db905750a62", size = 224910 }, + { url = "https://files.pythonhosted.org/packages/63/9e/b880f9ed61b73492e24bb962d76aeb63f18ccb895f0edfb52e20d45ed6f2/xxhash-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4931ea93840f750a908efebaf23c71004feacc1a4649ef601b96d400a505c9a9", size = 240742 }, + { url = "https://files.pythonhosted.org/packages/3f/89/fc682f93e54e486fc338b26a7d6d0d5cb0ab366269273c2608ac62b51afb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2fd4b60e8d9fc3923f39079f185b3425e6d76636fcb66d82a33dd7eba7c30f2f", size = 300527 }, + { url = "https://files.pythonhosted.org/packages/80/71/a4b4122afb2d17ad69e0922cfeddb5ad5c25b02f37eed3dd3819d42e5f55/xxhash-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1da00075f1605794298878cb587f7533329693e2a0c45bbd25d6353644add675", size = 443195 }, + { url = "https://files.pythonhosted.org/packages/d5/e5/ed3930f5dc90f4b1bab5ac3be099e8b2e81c1262d85e4adb5f2758e30d23/xxhash-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba73801c87d44fa37b2a5feab3004f0a654506027bf032ceb154d94bb74ea772", size = 217252 }, + { url = "https://files.pythonhosted.org/packages/44/ae/128ea5794387ca54bb4084566db20dbdfc9c21cb17b67d3fcb403927b5ba/xxhash-3.8.0-cp312-cp312-win32.whl", hash = "sha256:0b0836dee6022e22ba516ebfa8f76c6e4bda08d6c166c553e40867bac89e4a54", size = 31890 }, + { url = "https://files.pythonhosted.org/packages/4f/04/a6c182dc566c88e8d1a497d22cc4ffdcfcc0a9fa80325efa6cd4b9002c54/xxhash-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3bc2a09b98b8f85c75208cd2b2d2aecf40c77ecb2d72f6bf9757db51a98d3499", size = 32677 }, + { url = "https://files.pythonhosted.org/packages/93/b5/aeda4e79f962c8d58ec60cb20a5abfe91c9f7d62e626f69f6659bc0bd0c4/xxhash-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:208e6a8b93426896d803224e9fabe26f8b9c651e8381a80b1fa31812faa091e3", size = 29155 }, + { url = "https://files.pythonhosted.org/packages/ec/1f/96f43c5c7c7c4d44721f8d2e5d74698c667a30283c4b10a7e50a56804ee3/xxhash-3.8.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:36434c1d1b0a4729df1fa26ab11bffed1ba52666c0beb605c98a995b470cd143", size = 38508 }, + { url = "https://files.pythonhosted.org/packages/1c/d9/7d5d6af4876c6481f2e0acb2dda64dd5209574bf7ba1ad4f6af7a1f8d473/xxhash-3.8.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:a5e6497cefcb2d67f1745c66df9718a99112583af6cc2b70da0312a2eb939f1e", size = 36542 }, + { url = "https://files.pythonhosted.org/packages/32/ff/66fed439d78c5a09a1491a85af29bf8923b516530116731a9ac6b14dee2b/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5b00b82f1be708da9404fefd658cf5cf3be5ee3be2aae4bfe3b874255badd342", size = 31102 }, + { url = "https://files.pythonhosted.org/packages/56/b8/9fae0399281095f8aca1f32b21947b3c3c75ad6021b255c5c6e4b11d3866/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38b0cb0ab7f283413b7cace2bf710d7cf8f702ea82cbc683908691d52028a89b", size = 32096 }, + { url = "https://files.pythonhosted.org/packages/61/a4/e53d162c74a8a2950dc063969914387b0680da4c7c20ad17744ec03a3b0a/xxhash-3.8.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:084312171a9798dea85e924b2674f5e1a44933050a1ea1cb1c6b1364e004c66c", size = 34585 }, + { url = "https://files.pythonhosted.org/packages/69/f5/e12397e3f2c4917b6572e103a3277cd27cc56330e304bba61d195d7e5224/xxhash-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a1a9e845bd3bbc57d9356819e0d198fe23282e0576b398a6282a0f8fdc75aef", size = 34622 }, + { url = "https://files.pythonhosted.org/packages/70/80/c053dc51af5c942229689a0e9cb66fdc999bbd840f645e761f5ab73cbb17/xxhash-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ffbde09743ebaf8957b8426948fbe85eab5e5de0d29eec407fcff5a2812a3cc", size = 32320 }, + { url = "https://files.pythonhosted.org/packages/f8/a3/294171b67dfe770e1293edcf2a3f7e41302cdb8aefb258585312191b3ffe/xxhash-3.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a6dee3952c2b6e82e7f1dbc5dbc6167f9c84126851def7926e32827c2816169c", size = 220532 }, + { url = "https://files.pythonhosted.org/packages/80/c3/d141bfdeca785c8c680abf867d4b52a5e64a55d90df242c3141a3e58c4b2/xxhash-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf8ff8e12416c9fa05b43c7509b9332d6ffc4090413c4e7a1dee8599763b6d59", size = 241215 }, + { url = "https://files.pythonhosted.org/packages/09/5a/aeaf35143a6f3d44db73298e861405bdd9c9dacaedfc369cb43d9fd65282/xxhash-3.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cebbb322df4d97d8ef2704f49ed2f6f21f6702fafa0dc0c2a6ae70e904205689", size = 264615 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/f8ca782bb34f99693faab70a7989bcc84f62ffe93c9a4cca464a33507a4b/xxhash-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a8d08707b4100ebce598fc59fadf04b42d79b855818d6994f8f0fffd1df8edb", size = 242682 }, + { url = "https://files.pythonhosted.org/packages/d4/fa/ddbee4ff1542c2e88e72269a5a6bd18c3f26a80c2514e0918f5d1f3e9ec5/xxhash-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cf5427602dda15d8ce3c6d870d29bf07d43975f59c9d6d3f7f6f93a901b28b12", size = 473551 }, + { url = "https://files.pythonhosted.org/packages/25/f5/a680d48dddab37ab2fd9189ca03f775e29e3627122e30790816d7eb365af/xxhash-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97d7bd715ea5050b6c9638b52c62adf3055b648ef6eee6892a4cd9697b530191", size = 220485 }, + { url = "https://files.pythonhosted.org/packages/22/b1/7ac129b74981c07f1ff9c649f204465e86f83f9f29b2ebdc70d91514c365/xxhash-3.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cd25bbbab37d898f6e5a90905ce6ae2c1f8bd6668c07cef406fb3e8c8c570dd", size = 310307 }, + { url = "https://files.pythonhosted.org/packages/67/e6/43e673411249dd63f6cd974523a1b32fad75cf5453e363bc8f44af215fb9/xxhash-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3e30e5c057f483c3c53a11b53eba091a737cb19dfead36c8b23bf5beb4a169cd", size = 238164 }, + { url = "https://files.pythonhosted.org/packages/e5/95/87f8baf41f63130f3637104b7a610f82b20106332fc6e289c8dbf7955d0e/xxhash-3.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:07dd44d992ebd456752bc25b1c42cd172d94bd8cb24049300449ad0716081c3a", size = 269062 }, + { url = "https://files.pythonhosted.org/packages/38/c9/3369b497cd1f926b930c52fd2400606f177790d887b49f9e86bddcc24562/xxhash-3.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3118600a3102d4707dc1c485dbc3acbbbf37819069ad3e7854e77b923745d76b", size = 225007 }, + { url = "https://files.pythonhosted.org/packages/34/c8/03dceb86a8128858ac105bd6e282d62b3db6fd421a79bd8a9f6b8cdc47a7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ed37b0c95d8fb3fbaad5e13cc0a9727eb8739d1d54b2adef28108c250cada3a", size = 240815 }, + { url = "https://files.pythonhosted.org/packages/47/a5/ebd43eeb1af1dd8f0201943688b20958e99d3f6eb36481fb8c37b55ef139/xxhash-3.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bb043da412e478e7b1db3407051124b85b133803794d3809ad6d92870b304fc7", size = 300632 }, + { url = "https://files.pythonhosted.org/packages/df/24/c873e41a3c00dacc385c8ff08c007723f6a528922c1cea7fd9684e86dae7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:196fc132683d9311a0bdce8388ee52bfa07fdc1987cc428a27956e47ccd7b50d", size = 443293 }, + { url = "https://files.pythonhosted.org/packages/4f/1b/c671272fe28f70574e3c574d58465f26460154bcc68876121872afa1c14d/xxhash-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfb5411af3b77c75e99db100aa15c5ba623c85d72c565e4d7a0ed1a986ff766e", size = 217327 }, + { url = "https://files.pythonhosted.org/packages/57/43/b45a52f795812cb769b6ac159e69b605d18b1c067749e63dcac159e90064/xxhash-3.8.0-cp313-cp313-win32.whl", hash = "sha256:6d1d6179e26830c6690fac63f76d372f69714b977e12ca9c42188a60f51c59f5", size = 31898 }, + { url = "https://files.pythonhosted.org/packages/a1/42/2bd70e4eec25dc5990652979d708d4d7c999793d7d5af5d0e48ab4374dc1/xxhash-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7c92427a56a12f4d5c7bb26dbb9e9a4658c313ecb6c2f1dca349902e3822df07", size = 32680 }, + { url = "https://files.pythonhosted.org/packages/d5/c8/2fe61edb6144183cf094035a8c5354c65a073127acf6379655ed1e705b70/xxhash-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:9fc8453642c1c6d38b4fbac8901c2452ce1fa88b27f003bfee6703cbfae9bd63", size = 29157 }, + { url = "https://files.pythonhosted.org/packages/b9/b8/81d17a993b9a4750ba426ce966421681bb4b8e82a460cd346756491b8cc2/xxhash-3.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:efcacb644a915f010dc477447b045e5dcde1afaa40d16b2f0f8e7cd99c9e1635", size = 34897 }, + { url = "https://files.pythonhosted.org/packages/bb/3b/f5a368e3273440b3ea58fbd3f0b08c19f552b25ca59f43f5732ca96d2126/xxhash-3.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1e0dbc510cff94c5efbcc2b82c28b41519fad09b5b1f9f3d99c63e3940e49a0", size = 32630 }, + { url = "https://files.pythonhosted.org/packages/a6/ab/f424359c91c55f564fbbe4e454a126eb522471109f67376f20ad19c5e663/xxhash-3.8.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ff19d016a41c90d1f519005887191896b6da1274e1d5d48b347e17eb798ffc5a", size = 225874 }, + { url = "https://files.pythonhosted.org/packages/ac/c2/434579ef9235123b6c9bfa89c5614e0001e988613b91557b24aa326d9faa/xxhash-3.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aafc3eab99c50508852e34307e9565933bf128cad084cac7d2471b7ab1743de0", size = 249705 }, + { url = "https://files.pythonhosted.org/packages/c0/6c/3c0c917331ca3c71f826cedce2127f230624e2b49b992472dd5e9e72101c/xxhash-3.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e521368ed79ae6c4d31e1e417726643c49d7d6e286f4fdabf9a8330ed8a8ff7", size = 274716 }, + { url = "https://files.pythonhosted.org/packages/c1/f3/a8bb98d3307c67e88be9642dff52854c3de3f488f95989b60ff69c8dcc42/xxhash-3.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a0127688d116ec0c225e7e1f744e3f206de2b8822ffeb31a9ab5cc6384f92c5", size = 252019 }, + { url = "https://files.pythonhosted.org/packages/f7/73/fab69a2e5b6353dde643209fe9b6adf4fbd64c888e531deffc476bfb2635/xxhash-3.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:22c0b17da2f9fea0f8836538512249871b359141616bad44c58d238b5f011f40", size = 482024 }, + { url = "https://files.pythonhosted.org/packages/a3/5b/ba34099b5278097ec9c68c0b740719813553bfd11ca17e7353de6d2a41e3/xxhash-3.8.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d49465646b1a5e3b1729c5f636e05676a2fb52e203e3b22a5411c416c4c5302", size = 226655 }, + { url = "https://files.pythonhosted.org/packages/76/0c/90aba4708a37fe752b324a7cbf10058eaa33e892cdd62751ff17a5137b93/xxhash-3.8.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2853dea1e30ed00ca87dd87d76da5da063d302b823b3fb80ccd18421de0f251", size = 319583 }, + { url = "https://files.pythonhosted.org/packages/38/46/42e349e2d3017b2688f4cb301742c37c438e77963e3fef711edce2fc5c65/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:82f0102a2a3760287b7cd7f9e0a30edd4c3b18762ed1a242208d43c8e2bcf30b", size = 246000 }, + { url = "https://files.pythonhosted.org/packages/ee/15/741b947ae3c768e82018c46846f8616f6aa9b5042649f318a1a6897defe3/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:b8414a66a7524596d841cad5dc1adab6ce76848db5ab2b83db911fbdab1417af", size = 275455 }, + { url = "https://files.pythonhosted.org/packages/c6/b4/a9db84c9458fc8f53eaf0051377d1e9eecd9f330fb1225640027417a309d/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dbaa73df10414ea1e41b98691a9d8241d4c47ad8d02c726587a3cda05278e53", size = 231209 }, + { url = "https://files.pythonhosted.org/packages/20/92/60a868cd34851746d0b0d95dced0f42867c7c00606f6e5dba85b70b232ce/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:43fc9aaba10ab4267c90793601f60d35c3c9caa1544eceb483618a71ad9ce7da", size = 250416 }, + { url = "https://files.pythonhosted.org/packages/7a/6a/168ca46a4679c32aae9246caa1fddf35981d6304487e45e992b3d4530324/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ec5eb3d28fbb9802c6d2526f772133a06c91d6f03756fcc67c834b642ffdd51d", size = 309764 }, + { url = "https://files.pythonhosted.org/packages/18/0b/13646b348c07679c818791ab2d35415db5cb20f3bc77daaa255909a401b4/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2b77c301b644cd9b4d0749a3291081ec2048a6bef7fe0487c993bbba3efb9ce0", size = 448650 }, + { url = "https://files.pythonhosted.org/packages/59/9a/3d244b2acf6bbd86a363817ee09084b4684e8e11840663e19869e9e0d952/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d7ece11a132325353890a144c30119073617a1299c593ca29b96c315b07e1edd", size = 223572 }, + { url = "https://files.pythonhosted.org/packages/6f/c7/143410d026a6e0d86dc69037ec2a3b8db810a54e7f443b340ac17612be2e/xxhash-3.8.0-cp313-cp313t-win32.whl", hash = "sha256:b21db84df7b9d54d9e4195a964243c1b32d745c6fbc0cfcfffee1d4bd297196a", size = 32301 }, + { url = "https://files.pythonhosted.org/packages/6c/db/2240b0638161637b2f310231748a7a6a06c79fb43a3adb34c96f359762bf/xxhash-3.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0643b7d9f598f6da6f1f6b899f4358250d0fb853242e2d712cbde27bf5a99d29", size = 33221 }, + { url = "https://files.pythonhosted.org/packages/e0/d8/52038e4fa5baf4f00654a225516168d02908edfec7ca104fbefc58af394f/xxhash-3.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4bbacf2e938526969f8ab3334d4ac3da14ea059e1dfd1339a92f9091467e750f", size = 29294 }, + { url = "https://files.pythonhosted.org/packages/e5/ef/a09907aa28bdcdf6810d5c26656b154c60c0f06bb8db8442a1192d9c227a/xxhash-3.8.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:557e2a7cc0b6a634cf9c8e5c975d96b7da796fdeb1824569d760cf0f25b6f33f", size = 38365 }, + { url = "https://files.pythonhosted.org/packages/d2/4d/d991ff77bc489c2231025e64e570502156d573c7bff69c917589cc307089/xxhash-3.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:dad744d1613cbfddb844dad93adbffbd51c3e9f53ceea9568f7c3b94bedc19a4", size = 36477 }, + { url = "https://files.pythonhosted.org/packages/cc/0e/553eab001f1e274da73da074968cdc8be8cacfb318937ab9871b8e1909cb/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:953f29b22c04b123cf3cd2e08bccde3a73184aeda5a1038e0054cb3355644120", size = 31116 }, + { url = "https://files.pythonhosted.org/packages/55/d5/d0f4dbe7b4d9ce0125f16e45ec0be5e04f6a172edb4e2fa551c4f2eb5d7a/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:aa699e0253ceffecf41cae858d0a11f2439d6874a0890b556387bffe11dc1c08", size = 32112 }, + { url = "https://files.pythonhosted.org/packages/2e/2f/b332c7bede6a676343f2c9c8dea233c8c82753eaeda6f7a2c321d8c58ca3/xxhash-3.8.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e232c82466babc13e956d53aa84d0149660ed6886bc195248bb4d03bf2eca301", size = 34618 }, + { url = "https://files.pythonhosted.org/packages/b3/5b/2bf3c9e61c7cf8f53bce937af45e22b72bb1f224d5afb20352beba0d628d/xxhash-3.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f75fd1c6a5028f345cd4a8c52f4774d2e5b7809fa58111c60a5502b528914a4", size = 34739 }, + { url = "https://files.pythonhosted.org/packages/64/b6/e88521f5736c181b89bfb7ab756f0ca658a8a1ecece7277b75e167717614/xxhash-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b49d7e09b211a1ad658dbe2dbf6561eb92f2e6926bd1101e2d023178371f2d6f", size = 32332 }, + { url = "https://files.pythonhosted.org/packages/ed/a2/fba440739fa5f86d2c28738c202e88d3dd063290c8bbb20e183c5334456a/xxhash-3.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ceb702bc8e56b7f1f1413d42aa294045b9a0e4c9888e07edc5cd153e8c4c948f", size = 220479 }, + { url = "https://files.pythonhosted.org/packages/2c/1c/4a1639efec16416695d6c7bc6b224d3f607e0b8cbe2409fa81081a849d1c/xxhash-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f3c96e06bdb122e8cc84f5c7088579f3102b828efd62e9dc964a9d17c7b89e", size = 241409 }, + { url = "https://files.pythonhosted.org/packages/92/d1/8ce471f8d6752384f972fd5f6363f2e8d8b867a89fbd724c6dbd91d2bb98/xxhash-3.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:415a8d06ac9bea36b1e06b603a347e0f62401042a97d7bfccec8ae2da12ad784", size = 264433 }, + { url = "https://files.pythonhosted.org/packages/95/77/400a281683fd39c54e2ac497fa67bdf886baaadb8c0ba58f7e1ea1d7692e/xxhash-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f5ccdd2deb5dce31201cc0eec94388cce97e681429073db50903fab0a0a8a0d", size = 242835 }, + { url = "https://files.pythonhosted.org/packages/aa/a6/edda651cfa0ba8e921791e93468fae655b63894d89730fcbfe46704f0d0a/xxhash-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a6cf81bc699d3a5ebfcf2fdb2a7bd2e096708d7de193f6f322944a02ba00953", size = 473800 }, + { url = "https://files.pythonhosted.org/packages/dd/da/50f764ec6a93d3961fce294567e41bfca0e66d168deed354a3dc90ebeba6/xxhash-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4d12a04d7ffc0359f0eadc4535a53cab113044c8d2f262c7e9a56950a5ed50e", size = 220677 }, + { url = "https://files.pythonhosted.org/packages/bb/49/9fe4ed5aac6f38629cc83b34f84748b83ad8295a578ec6a49d8bf896cafb/xxhash-3.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d209373fcb66138c652cf843385ee60866e50158a7869bbbf8b322d9a822b765", size = 310385 }, + { url = "https://files.pythonhosted.org/packages/83/f5/1147e03c0553ed22bbae9ce47503c37ee0c5f95592aae10f339c25f61de9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b88a3fe28277811e599efa6e1c96abce8a77d60dd79c94da7a9b5c377c172b7b", size = 238330 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/92daf66c1966c84da5c97a06ced1480208d3a3bd465cb0630565ec00d1b9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5d5a888a5ef997cb35f1aad346eb861cd87ecfe24f5e25d5aa4c9fd1bd3950c2", size = 268667 }, + { url = "https://files.pythonhosted.org/packages/bd/c0/080c1a92972667e183c04b03f33c877f8ec61cfa3570e61731077286648d/xxhash-3.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:de2836e0329c01555957a603dcd113c337c577081153d691c12a51c5be3282b0", size = 224934 }, + { url = "https://files.pythonhosted.org/packages/d9/d5/cbc4e5b2bee10c94cba05b5bb2b8033e7ef44ae742583fdafcd9188e33ed/xxhash-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4bc74eedb0dd5827b3be748bacf9fdb50004037a3e16c7ddb5defae2682cef71", size = 240870 }, + { url = "https://files.pythonhosted.org/packages/76/f7/09679b00e192b741b65c230440c4f7e6df3251a9ad427a518ddf262ec71a/xxhash-3.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c571b03d59e339b010dc84f15a6f1cff80212f3a3116c2a71e2303c95065b1f6", size = 300683 }, + { url = "https://files.pythonhosted.org/packages/5f/1b/f43ec36e8c6a20c77be0bcca23f0b133ed8a0312681500d1676eebd71924/xxhash-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:87626acdd6e2d762c588a4ffe94258c5ef34fb6049a4a3b25019bdb7f9267a9b", size = 443407 }, + { url = "https://files.pythonhosted.org/packages/45/2e/a3e3a779c5e4789daf975e05cc1c7f11bae724a03855120029d4592c8e63/xxhash-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:076d8a4fb290af952826922aa42a46bfc64caa31662ce4e2925a445d0e6ce57f", size = 217559 }, + { url = "https://files.pythonhosted.org/packages/44/da/1c1e078ac290afff304a541a2a60965beb369ad65b4f30ec93ea1e0b7210/xxhash-3.8.0-cp314-cp314-win32.whl", hash = "sha256:52f8c7c9833d947e60df830671f6eca810d7c667051243985a561c79f1a3d545", size = 32602 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/d455cb83d5e3c94046234294fb5dbbe5da600d1bbdf76b9527756920cce9/xxhash-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:4fbfcb7dd307e23189a71050f6e27746926590330f37d5fd2ffcb8ea78de1f42", size = 33393 }, + { url = "https://files.pythonhosted.org/packages/89/8f/1b14471f617bc96edbb9566099a162d918a981381c398114726cc600b76c/xxhash-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:ecef1e65b4715c7326002073763fe94cc44c756a0698508abb915ab3d6be6e3d", size = 30007 }, + { url = "https://files.pythonhosted.org/packages/11/8d/51ad2f9f784121c8057ef1ba36362f58d4595cbcad16322941f5b73eb53d/xxhash-3.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:02ed856a765cb6e006168595d9455ac8c3c4d60cc04cd47a158a1ac677d68f0f", size = 34957 }, + { url = "https://files.pythonhosted.org/packages/1b/14/175c573ae4fac48bf21a82e5b9ceec75d64c520c51ca08de3105de539438/xxhash-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eec30461a7b457611098ba7ab09363e36c8b2645b4687fb6f3d405bb646e3410", size = 32635 }, + { url = "https://files.pythonhosted.org/packages/96/08/f83efabd350a50c31c851b88891e318a6f07bdbf40a43d0f7bb6cedade7f/xxhash-3.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b471744912d1ce5dd6d3975b7525e77518359ebf3aa1bd7d501e199f5ae488ea", size = 225969 }, + { url = "https://files.pythonhosted.org/packages/7c/78/2b6d12da9cf572c84d93b88ecbf9bf6539a7c5219bde128b214396b97c8b/xxhash-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3748d71202bf3f279e77cb8b273b6d0f29d1bcaefb6ce6cb03b95f358863ba37", size = 249851 }, + { url = "https://files.pythonhosted.org/packages/d0/0a/755eeb1882634983b24e6375a95ed233228dc48f0ef12655388bf3c7eeaf/xxhash-3.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b3bf59ea94b2a23b0f992769804ab9401d5cdcd9df0062fe2cd78a491ae8851", size = 274842 }, + { url = "https://files.pythonhosted.org/packages/77/f2/09b1231cad17c314e51664c4a004c919108ec59aba10f9a28fa061e7b8be/xxhash-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:40f061aa5379eba249e9367b179515571e632be6d1b6f55ac139e6fe3d08463c", size = 252218 }, + { url = "https://files.pythonhosted.org/packages/b2/24/de756d55547953494eb6775aea92e258035647b3ecb8547618cd549001e1/xxhash-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:680d70896a61fc920cc717a0a8fe8a9fb5858c563184666e31874caa54a16d9e", size = 482135 }, + { url = "https://files.pythonhosted.org/packages/e5/63/b8147633e32f98ef2b4bb0dfca82f0f63e2b02ff179f20664af64c4216a7/xxhash-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14973fbdee136588e57447401b521f466a42faca41eecdf35123c73103512ca8", size = 226776 }, + { url = "https://files.pythonhosted.org/packages/29/37/ba051d8f0380d3cf845b23ba058a17d32025846463eb6bf885887fc8effe/xxhash-3.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:96c6bca2486cdc58b125966817a92a6abe6ef1fab86b2f8798a7e93488782540", size = 319738 }, + { url = "https://files.pythonhosted.org/packages/4f/6f/36e0a27dd27ffa3f7b521650cbcd52a00fb86b71343ffadb642374e8263c/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b1109ae238e932d8482f9cb568b56a405cc73bc7a36b837844087f1298dd218", size = 246136 }, + { url = "https://files.pythonhosted.org/packages/fe/73/2663dbf4c09386a9dcc8a94d7a14b4609ed4bad8180ced5b848e60a9b660/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1da5db0863400eade7c5a31969754d1392189f26b4105f6631da2c6c7ea3bccc", size = 275568 }, + { url = "https://files.pythonhosted.org/packages/d6/58/f3ce1bc3bb3971191f6521273ddae98d3c610bcefbbed5327c3b3627c12f/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c61b5a0f21ace5e886f177cce43826d85a7c84e35a9e17cb6d1b4ac0b7a7d833", size = 231314 }, + { url = "https://files.pythonhosted.org/packages/4d/51/835706a36cdc00e5b638fba9b22218b3d40d23a7677c923feca8a3f55b98/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1db4f27835a450c7e729bc9330c6e702113711cea1f873d646e3a31fe96a9732", size = 250521 }, + { url = "https://files.pythonhosted.org/packages/c1/47/b0b62caa3caee58ab9de8969f66aef1c3729886f3ff60e173fda3f2762be/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4788a470f946df34383abc6cd345088c13f897a5ee580c4cdd12b1d32ad218ef", size = 309926 }, + { url = "https://files.pythonhosted.org/packages/69/c4/60e6d18a0e131c7af622374af9deede15d3c47d8e5e7221933481b57b319/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3b6dfa83096cb1e54d082acebaf67f0c42667c56dc48ba536a76cac08d46391e", size = 448812 }, + { url = "https://files.pythonhosted.org/packages/12/9f/c9627daa052be39a932d0e17c6bf6a9041d2cde3afacbded9196acf70261/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:57ec0ba5299a9a7df376063c139f5826ff0c89b438703939af3d252c31ca96a4", size = 223639 }, + { url = "https://files.pythonhosted.org/packages/a9/38/92916e008a84c1f1a9aef82e4363cdc478a722ff69e59c6afbf93d3d1fda/xxhash-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:d9a61f23b999baeb84102aba767b1b3e94958eab94e6c11b08927e7dc4200795", size = 33078 }, + { url = "https://files.pythonhosted.org/packages/31/7c/e413bc75121d9628bf023b2ed251411ca3a447cf00cd9aa3438ab17f6c67/xxhash-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:61069b260fff84116235bb93845f319284dc6b42527c215af59264f4c2ee3468", size = 33953 }, + { url = "https://files.pythonhosted.org/packages/f6/eb/21a96e218375bd8b6ecd6d07cf60c8ff1a046e93cdedc3cf7bc3309edf7b/xxhash-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:73cecd431b4f572d38fcf1a7fe85b30eb987778ef9e7a70bc9ffcf2d64810e6f", size = 30164 }, ] [[package]] @@ -4592,132 +4944,132 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, - { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, - { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, - { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, - { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, - { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, - { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, - { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, - { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, - { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, - { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, - { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, - { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, - { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, - { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, - { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957 }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164 }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688 }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902 }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931 }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030 }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392 }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612 }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487 }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333 }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025 }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507 }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719 }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438 }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719 }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901 }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229 }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978 }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733 }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113 }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899 }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862 }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060 }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613 }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012 }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887 }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620 }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599 }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604 }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161 }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619 }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362 }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667 }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069 }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670 }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916 }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625 }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574 }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534 }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481 }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529 }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338 }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147 }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272 }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962 }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063 }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438 }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458 }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589 }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424 }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248 }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084 }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272 }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497 }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002 }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165 }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128 }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382 }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964 }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204 }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510 }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584 }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410 }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980 }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219 }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576 }, ] [[package]] name = "zstandard" version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, - { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738 }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436 }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019 }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012 }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148 }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652 }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993 }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806 }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659 }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933 }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008 }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517 }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292 }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237 }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922 }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276 }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679 }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735 }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440 }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070 }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001 }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120 }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230 }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173 }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736 }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368 }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022 }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889 }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952 }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054 }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113 }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936 }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232 }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671 }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887 }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658 }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849 }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095 }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751 }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818 }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402 }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108 }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248 }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123 }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591 }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513 }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118 }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940 }, ]