commit f468845f6761c690778225ec2a77567b515509fb Author: chakcy <947105045@qq.com> Date: Mon Sep 21 14:06:55 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ee8b1 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# chongming-license-gui + +机器码授权的**可视化工具(厂商侧)**:密钥管理 / 签发 / 查看校验。 + +供商务、售后等不习惯命令行的同学使用,功能与 `uv run chongming-license`(CLI)对等。 +所有授权逻辑都调用 [`chongming-license`](../license/) 库 —— 本包**只做界面**, +不重复实现密码学、载荷格式或规范化(一旦有两处实现,就会出现"自己签的自己验不过")。 + +## 定位 + +| | 使用者 | 形态 | 签发能力 | +| --- | --- | --- | --- | +| **本工具** | 厂商(商务/售后/研发) | 桌面 GUI | ✅ 持有私钥 | +| `chongming-license` CLI | 厂商(研发/自动化) | 命令行 | ✅ 持有私钥 | +| 设备端「授权」页 | 客户 / 现场装机 | 浏览器 `/static/license` | ❌ 只导出申请码、导入 license | + +**本工具不随部署产物分发** —— 签发私钥只应存在于厂商机器上。 + +## 安装与运行 + +```bash +cd utils/license-gui +uv sync +uv run chongming-license-gui +``` + +无图形环境(SSH / CI)下可做冒烟检查: + +```bash +QT_QPA_PLATFORM=offscreen uv run python -c " +from PySide6.QtWidgets import QApplication +from chongming_license_gui.app import MainWindow +app = QApplication([]); w = MainWindow(); w.show() +print('界面构建成功') +" +``` + +## 三个页面 + +### 签发(默认页) + +日常最高频的操作。让操作者在**按下签发之前**就看清"给谁、哪台机器、哪些插件、多久"。 + +- **签名私钥**:显示当前生效的私钥路径与公钥指纹(签发前核对,避免拿错密钥) +- **签发依据**(三选一): + - *申请码*(推荐)—— 粘贴 `CM1-...`,自动解析出机器码与各来源指纹,支持换硬件容错 + - *来源 JSON 文件* —— `show --json` 的导出文件 + - *仅机器码* —— 精确匹配、无容错(界面会明确警示) +- **授权插件**:勾选式列表,自动从 `<项目根>/config/workers/*/config.toml` 读出本项目实际插件; + 也可手工添加任意 type;支持全选/全不选/移除 +- **授权信息**:客户、授权编号(留空自动生成)、备注、密钥标识、有效期(N 天 / 指定日期 / 永久) +- **预览与签发**:载荷 JSON 预览(与签名用的是同一个对象,所见即所签)→ 签发并保存 + +### 台账 + +已签发授权的检索与管理。**签发页签发的每一份都会自动入库**(含机器指纹)。 + +- **检索**:客户 / 机器码 / 插件 / 状态 / 关键字 / 即将到期窗口;按到期日升序,最急的在前 +- **概览**:总数与有效/即将到期/已过期/永久/未生效计数 +- **续期**:选中一条 →「续期选中…」→ 只改要变的部分 → 续期并保存。 + **指纹复用自台账,无需再向客户索取申请码**;默认生成新编号(新旧都留台账可追溯), + 旧记录自动加上 `已续期 → <新编号>` 批注 +- **备注**:人工批注,重新导入 license 不会覆盖(载荷里的 `note` 是签名内容的一部分, + 台账的 `note` 是厂商自己的批注) +- **详情**:查看各来源指纹与**原始 license 文件内容**(当初签发的那份) +- **导出**:`.lic`(可选按客户分目录)+ CSV 清单(UTF-8 BOM,Excel 中文不乱码) + +台账库默认 `~/.chongming/licenses.db`,可在此页切换;单文件 SQLite,可直接进备份。 + +### 密钥 + +- **生成密钥对**:一键生成 Ed25519 密钥对,私钥写为 `0600` 的 PEM, + 公钥自动复制到剪贴板(下一步注入构建就要用) +- **加载现有私钥**:显示对应公钥与指纹,用于确认"这把私钥对应哪把公钥" +- 公钥 base64(可直接填进 `[license] public_key`)与 PEM 都可单独复制 + +### 查看 / 校验 + +- 打开 `.lic` 查看全部字段(不验签也能看) +- 填公钥后验签,给出结论 + 原因码 + 命中/不匹配的来源 +- **指纹比对基准**可选: + - *本机指纹* —— 在厂商机器上通常对不上,属正常 + - *指定机器*(填客户设备的申请码)—— **发货前自检**的关键: + 确认这份 license 确实是为目标设备签发的,而不是只验出"签名有效" + +## 与 CLI 的对应关系 + +| GUI 操作 | CLI | +| --- | --- | +| 详情面板 / 状态栏 | `chongming-license verify` | +| 签发 → 签发并保存 | `chongming-license issue` | +| 密钥 → 生成密钥对 | `chongming-license keygen` | +| 密钥 → 加载私钥(显示公钥/指纹) | `chongming-license keygen` 的输出 / `inspect --key` | +| 查看 / 校验 → 仅查看内容 | `chongming-license inspect` | +| 查看 / 校验 → 指定机器 | `chongming-license verify --request-code` | + +批量签发(一次几十台)建议直接用 CLI 脚本化;GUI 面向单台/少量、需要人工核对信息的场景。 + +## 安全注意 + +- 本工具会在 `QSettings` 里记住**私钥路径、客户名、备注、输出目录**(便于连续签发), + **不会**记住私钥内容本身。 +- 私钥文件权限为 `0600`,且 `.gitignore` 忽略 `license_private*.pem`。 +- `uv run build` 会扫描构建产物,发现私钥即终止构建。 +- ⚠ 私钥丢失 = 无法再签发新授权;私钥泄漏 = 任何人都能签发授权。请离线备份。 + +## 完整文档 + +见仓库 [docs/license.md](../../docs/license.md):激活流程、命令行参考、机器指纹与容错判定、 +强制执行点、安全边界、故障排查(含原因码全表)。 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..95aee84 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "chongming-license-gui" +version = "0.1.0" +description = "chongming 机器码授权可视化工具(厂商侧):密钥管理 / 签发 / 查看校验" +readme = "README.md" +requires-python = ">=3.12" +dependencies = ["PySide6", "chongming-license"] + +[tool.hatch.build.targets.wheel] +packages = ["src/chongming_license_gui"] + +[[tool.uv.index]] +url = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" + +[tool.uv.sources] +# 本地开发用 path 引用同仓的 chongming-license,并设为 **editable**: +# 非 editable 的 path 依赖会被构建成 wheel 装进本环境,改库代码后必须重新 sync +# 才生效(否则会照旧导入旧版本,报"cannot import name ...",极易误判为代码有错)。 +# 发布后应改为内部索引的 registry 依赖(与 chongming-auth / chongming-cache 一致), +# 届时删掉本节与 editable 设定即可。 +# chongming-license = { path = "../license", editable = true } + +[project.scripts] +chongming-license-gui = "chongming_license_gui.app:main" diff --git a/smoke_test.py b/smoke_test.py new file mode 100644 index 0000000..981de9d --- /dev/null +++ b/smoke_test.py @@ -0,0 +1,284 @@ +"""GUI 冒烟测试(无头运行,驱动真实控件而非只构造窗口)。 + +覆盖:生成密钥对(含 0600 权限)→ 私钥状态跨页同步 → 解析申请码(含 weak 标注) +→ 插件列表自动发现 → 载荷预览 → 签发 → 查看/校验(目标机通过、异机拒绝) +→ 有效期三种模式的取值。 + +运行(需要 PySide6,故用本包的独立环境):: + + cd utils/license-gui + uv run python smoke_test.py + +注意:界面里的信息框/确认框会**阻塞事件循环等待点击**,无头环境下必须先替换掉, +否则测试会挂住。这也是本测试唯一需要 monkeypatch 的地方。 +""" +from __future__ import annotations + +import json +import sys +import tempfile +from pathlib import Path + +from PySide6.QtCore import QSettings +from PySide6.QtWidgets import QApplication + +failures: list[str] = [] + + +def check(name: str, cond: bool, detail: str = "") -> None: + print(f"[{'PASS' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else "")) + if not cond: + failures.append(name) + + +def main() -> int: + # QApplication 实例必须保活,否则控件无法创建;引用由 Qt 内部持有,故不进变量 + QApplication.instance() or QApplication([]) + QSettings("chongming", "license-gui").clear() # 初始状态确定,不受上次运行影响 + + from chongming_license import ( + build_license, + collect_fingerprints, + encode_request_code, + machine_code, + read_license, + write_license, + ) + + from chongming_license_gui import ( + common, + inspect_tab, + issue_tab, + keys_tab, + registry_tab, +) + from chongming_license_gui.app import MainWindow + + # 把模态弹窗换成记录器(否则会阻塞等待点击) + dialogs: list[tuple[str, str]] = [] + + def _mk(kind: str): + def _f(parent, title, text, *a, **k): + dialogs.append((kind, title)) + return True + + return _f + + for mod in (keys_tab, issue_tab, inspect_tab, registry_tab): + mod.info = _mk("info") + mod.warn = _mk("warn") + mod.error = _mk("error") + if hasattr(mod, "ask_overwrite"): + mod.ask_overwrite = _mk("ask") + common.ask_overwrite = _mk("ask") + + w = MainWindow() + w.show() + + check("四个页面就绪", + [w.tabs.tabText(i) for i in range(w.tabs.count())] + == ["签发", "台账", "密钥", "查看 / 校验"], + str([w.tabs.tabText(i) for i in range(w.tabs.count())])) + + # ---- 1) 生成密钥对 ---- + tmp = Path(tempfile.mkdtemp()) + priv = tmp / "priv.pem" + w.keys_tab.out_edit.setText(str(priv)) + w.keys_tab._generate() + + check("私钥已生成并加载", w.signing_key.loaded and priv.is_file()) + check("私钥权限为 0600", (priv.stat().st_mode & 0o777) == 0o600, + oct(priv.stat().st_mode & 0o777)) + check("公钥指纹已算出", len(w.signing_key.fingerprint) == 16, + w.signing_key.fingerprint) + check("标题显示公钥指纹", w.signing_key.fingerprint in w.windowTitle(), + w.windowTitle()) + + # ---- 2) 私钥状态跨页同步 ---- + check("签发页同步到私钥", w.signing_key.fingerprint in w.issue_tab.key_fp.text(), + w.issue_tab.key_fp.text()) + + # ---- 3) 解析申请码 ---- + src = collect_fingerprints() + rc = encode_request_code(src) + it = w.issue_tab + it.rc_edit.setPlainText(rc) + it._parse_request_code() + + check("解析出机器码", it._machine_code == machine_code(src), it._machine_code) + check("解析出来源指纹", it._sources == src, str(sorted(it._sources))) + check("weak 来源被标注为不参与判定", + "不参与判定" in it.target_result.text(), it.target_result.text()) + check("来源明细已展示", + len(it.sources_view.toPlainText().splitlines()) == len(src)) + + # ---- 4) 插件列表自动发现 ---- + types = [it.plugin_list.item(i).text() for i in range(it.plugin_list.count())] + check("从 config/workers 自动列出插件", types == ["aibox_sophon_adapter", "ip_sound_column"], + str(types)) + + it._set_all_checked(True) + check("全选生效", it._checked_plugins() == types, str(it._checked_plugins())) + + # 未勾选插件时应拒绝生成 + it._set_all_checked(False) + it._refresh_preview() + check("未勾选插件时预览给出提示", + "至少勾选一个" in it.preview.toPlainText(), it.preview.toPlainText()[:40]) + it._set_all_checked(True) + + # ---- 5) 载荷预览 ---- + it.customer_edit.setText("GUI 测试客户") + it.note_edit.setText("合同备注") + it.days_spin.setValue(30) + it._refresh_preview() + preview = json.loads(it.preview.toPlainText()) + check("预览含全部授权插件", preview["plugins"] == types, str(preview["plugins"])) + check("预览含客户名", preview["customer"] == "GUI 测试客户") + check("预览含来源指纹", preview["sources"] == src) + + # ---- 6) 签发 ---- + out = tmp / "license.lic" + payload = it._build_payload() + doc = build_license(payload, w.signing_key.key, kid="gui-test") + write_license(doc, out) + check("license 文件已落盘", out.is_file(), str(out)) + + # ---- 7) 查看/校验 ---- + vt = w.inspect_tab + vt.lic_edit.setText(str(out)) + vt.pub_edit.setText(w.signing_key.public_b64) + + vt._show_only() + check("仅查看不验签也能看到编号", payload["lic_id"] in vt.detail.text()) + + vt.rb_remote.setChecked(True) + vt.rc_edit.setText(rc) + vt._run() + check("目标机器校验通过", "✓" in vt.verdict.text(), vt.verdict.text()) + + other = {k: ("ffffffff" if k == "mac" else v) for k, v in src.items()} + other["machine_id"] = "anothervalueanothervalue" + vt.rc_edit.setText(encode_request_code(other)) + vt._run() + check("异机校验被拒绝", "✗" in vt.verdict.text(), vt.verdict.text()) + + # 无公钥时应提示而不是静默失败 + vt.pub_edit.setText("") + before = len(dialogs) + vt._run() + check("缺公钥时给出提示", len(dialogs) > before, str(dialogs[-1] if dialogs else None)) + + # ---- 8) 有效期三种模式 ---- + it.rb_forever.setChecked(True) + it._refresh_preview() + check("永久模式 expires_at 为 None", + json.loads(it.preview.toPlainText())["expires_at"] is None) + + it.rb_days.setChecked(True) + it._refresh_preview() + days_value = json.loads(it.preview.toPlainText())["expires_at"] + check("N 天模式给出到期日", bool(days_value), str(days_value)) + + it.rb_date.setChecked(True) + it._refresh_preview() + check("指定日期模式生效", + json.loads(it.preview.toPlainText())["expires_at"] + == it.date_edit.date().toString("yyyy-MM-dd")) + check("不勾选时对应输入框被禁用", + it.days_spin.isEnabled() is False and it.date_edit.isEnabled() is True) + + # ---- 9) 台账页:签发即入库、检索、续期、导出 ---- + rt = w.registry_tab + db = tmp / "licenses.db" + rt.db_edit.setText(str(db)) + rt._open_db(silent=True) + check("台账库已打开", w.registry.is_open and w.registry.path == db, str(w.registry.path)) + check("新库为空", rt.table.rowCount() == 0, str(rt.table.rowCount())) + + # 签发页的"签发即入库"是台账的数据来源,这里直接调 holder 模拟该动作 + check("签发页默认勾选「记入台账」", it.record_check.isChecked()) + w.registry.save(doc, public_key_fp=w.signing_key.fingerprint, source="smoke") + rt.refresh() + check("入库后台账可见", rt.table.rowCount() == 1, str(rt.table.rowCount())) + + row0 = [rt.table.item(0, c).text() for c in range(9)] + check("台账显示客户", row0[1] == "GUI 测试客户", row0[1]) + check("台账显示机器码", row0[2] == it._machine_code, row0[2]) + check("台账显示状态与剩余天数", "即将到期" in row0[4] and "d)" in row0[4], row0[4]) + check("台账显示备注", row0[7] == "合同备注", row0[7]) + + # 筛选 + rt.machine_edit.setText(it._machine_code[:12]) + rt.refresh() + check("按机器码筛选命中", rt.table.rowCount() == 1, str(rt.table.rowCount())) + rt.machine_edit.setText("ZZZZ-NOT-EXIST") + rt.refresh() + check("筛选无结果时为空表", rt.table.rowCount() == 0, str(rt.table.rowCount())) + rt._reset_filters() + check("重置后恢复全量", rt.table.rowCount() == 1, str(rt.table.rowCount())) + + # 续期对话框:默认生成新编号、复用原指纹 + from chongming_license_gui.registry_tab import RenewDialog + + rec = rt.records[0] + dlg = RenewDialog(rec) + check("续期默认生成新编号(新旧都留台账)", dlg.values()["keep_lic_id"] is False) + dlg.rb_keep_id.setChecked(True) + check("可切换为沿用原编号", dlg.values()["keep_lic_id"] is True) + dlg.rb_keep_id.setChecked(False) + dlg.days_spin.setValue(365) + vals = dlg.values() + check("续期预填原插件", vals["plugins"] == rec.plugins, str(vals["plugins"])) + check("续期复用原指纹(无需再要申请码)", rec.sources == it._sources) + + # 执行续期(走与界面相同的构造路径) + from chongming_license import build_license as _bl, build_payload as _bp, write_license as _wl + + np = _bp(machine_code=rec.machine_code, plugins=vals["plugins"], sources=rec.sources, + customer=vals["customer"], note=vals["note"], expires_at=vals["expires_at"]) + ndoc = _bl(np, w.signing_key.key, kid=rec.kid) + _wl(ndoc, tmp / "renewed.lic") + new_rec = w.registry.save(ndoc, public_key_fp=w.signing_key.fingerprint, source="renew") + reg = w.registry.ensure_open() + reg.update_note(rec.lic_id, f"{rec.note};已续期 → {new_rec.lic_id}") + rt.refresh() + check("续期后新旧记录都在台账", rt.table.rowCount() == 2, str(rt.table.rowCount())) + check("新授权为有效状态", new_rec.status() == "active", new_rec.status()) + check("旧记录留下续期链", + f"已续期 → {new_rec.lic_id}" in reg.get(rec.lic_id).note, + reg.get(rec.lic_id).note) + check("续期后文件可独立验签", + read_license(tmp / "renewed.lic", w.signing_key.public_b64, + sources=it._sources).ok) + + # 编辑备注:人工批注不被重新导入覆盖 + reg.update_note(new_rec.lic_id, "人工批注") + reg.save(ndoc, public_key_fp=w.signing_key.fingerprint) + check("重新入库不覆盖人工批注", + reg.get(new_rec.lic_id).note == "人工批注", reg.get(new_rec.lic_id).note) + + # 导出 + files = reg.export_licenses(tmp / "archive", rt.records, subdir_by_customer=True) + check("导出 .lic 数量正确", len(files) == 2, str([f.name for f in files])) + csv_path = reg.export_csv(tmp / "台账.csv", rt.records) + check("CSV 含表头 + 数据行", + len(csv_path.read_text(encoding="utf-8-sig").splitlines()) == 3) + check("CSV 为 UTF-8 BOM(Excel 中文不乱码)", + csv_path.read_bytes()[:3] == b"\xef\xbb\xbf") + + # 删除 + check("删除记录", reg.delete(new_rec.lic_id) is True) + rt.refresh() + check("删除后台账少一条", rt.table.rowCount() == 1, str(rt.table.rowCount())) + + print("\n" + "=" * 56) + if failures: + print(f"❌ {len(failures)} 项未通过: {failures}") + return 1 + print("✅ GUI 冒烟测试全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/chongming_license_gui/__init__.py b/src/chongming_license_gui/__init__.py new file mode 100644 index 0000000..08d4abf --- /dev/null +++ b/src/chongming_license_gui/__init__.py @@ -0,0 +1,15 @@ +"""chongming 机器码授权可视化工具(厂商侧)。 + +三个页面:密钥管理 / 签发 / 查看校验。所有授权逻辑(指纹、载荷格式、规范化、 +验签、容错判定)都调用 ``chongming_license``,本包只做界面 —— 不重复实现密码学。 + +运行:: + + cd utils/license-gui + uv run chongming-license-gui + +无图形环境(SSH / CI)下可用 ``QT_QPA_PLATFORM=offscreen`` 做冒烟检查。 +""" +from .app import main + +__all__ = ["main"] diff --git a/src/chongming_license_gui/__main__.py b/src/chongming_license_gui/__main__.py new file mode 100644 index 0000000..eaaf68a --- /dev/null +++ b/src/chongming_license_gui/__main__.py @@ -0,0 +1,4 @@ +"""``python -m chongming_license_gui`` 入口。""" +from .app import main + +main() diff --git a/src/chongming_license_gui/app.py b/src/chongming_license_gui/app.py new file mode 100644 index 0000000..4ce82b2 --- /dev/null +++ b/src/chongming_license_gui/app.py @@ -0,0 +1,90 @@ +"""主窗口与入口。 + +窗口标题里带公钥指纹:同时开多个实例或截图沟通时,能一眼确认"当前用的是哪把密钥"。 +""" +from __future__ import annotations + +import sys + +from PySide6.QtCore import QSettings +from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QTabWidget, QVBoxLayout, QWidget + +from .inspect_tab import InspectTab +from .issue_tab import IssueTab +from .keystore import SigningKey +from .keys_tab import KeysTab +from .registry_store import RegistryHolder +from .registry_tab import RegistryTab + +ORG = "chongming" +APP = "license-gui" + + +class MainWindow(QMainWindow): + """授权工具主窗口。""" + + def __init__(self) -> None: + super().__init__() + self.setWindowTitle("chongming 机器码授权工具") + self.resize(1180, 940) + + self.settings = QSettings(ORG, APP) + self.signing_key = SigningKey() + self.registry = RegistryHolder() + + self.tabs = QTabWidget() + self.keys_tab = KeysTab(self.signing_key, self.settings) + self.issue_tab = IssueTab(self.signing_key, self.registry, self.settings) + self.inspect_tab = InspectTab(self.settings) + self.registry_tab = RegistryTab(self.signing_key, self.registry, self.settings) + + # 签发放第一个:这是日常最高频的操作 + self.tabs.addTab(self.issue_tab, "签发") + self.tabs.addTab(self.registry_tab, "台账") + self.tabs.addTab(self.keys_tab, "密钥") + self.tabs.addTab(self.inspect_tab, "查看 / 校验") + # 切到台账页时自动刷新:签发后切过去应立即看到刚入库的那条 + self.tabs.currentChanged.connect(self._on_tab_changed) + + central = QWidget() + layout = QVBoxLayout(central) + layout.setContentsMargins(10, 10, 10, 10) + layout.addWidget(self.tabs) + + hint = QLabel( + "厂商侧工具:签发私钥只应存在于本机,切勿随部署产物分发。" + "设备端展示机器码/申请码请用 http://<设备IP>:8000/static/license" + ) + hint.setWordWrap(True) + hint.setStyleSheet("color: #666;") + layout.addWidget(hint) + + self.setCentralWidget(central) + + self.signing_key.changed.connect(self._refresh_title) + self._refresh_title() + + def _on_tab_changed(self, index: int) -> None: + if self.tabs.widget(index) is self.registry_tab: + self.registry_tab.refresh() + + def _refresh_title(self) -> None: + base = "chongming 机器码授权工具" + if self.signing_key.loaded: + self.setWindowTitle(f"{base} —— 公钥指纹 {self.signing_key.fingerprint}") + else: + self.setWindowTitle(f"{base} —— 未加载私钥") + + +def main() -> None: + """console script 入口。""" + app = QApplication(sys.argv) + app.setApplicationName(APP) + app.setOrganizationName(ORG) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/src/chongming_license_gui/common.py b/src/chongming_license_gui/common.py new file mode 100644 index 0000000..e1808be --- /dev/null +++ b/src/chongming_license_gui/common.py @@ -0,0 +1,148 @@ +"""可视化组件与小工具(各 Tab 共用)。 + +这里刻意不含任何授权逻辑 —— 密码学、载荷格式、规范化、判定一律调用 +``chongming_license``。GUI 只负责输入与展示,避免与 CLI 出现第二套实现 +(尤其是规范化 JSON:一旦两处实现有差异,就会出现"自己签的自己验不过")。 +""" +from __future__ import annotations + +import tomllib +from pathlib import Path + +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont, QFontDatabase, QGuiApplication +from PySide6.QtWidgets import ( + QFormLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +# 备份的常见插件类型:项目根不在手边(例如商务同事只拿到工具)时的兜底候选 +FALLBACK_PLUGIN_TYPES = ("aibox_sophon_adapter", "ip_sound_column") + + +def mono_font(size: int = 0) -> QFont: + """等宽字体:机器码 / 申请码 / 指纹这类需要逐字符核对的文本必须等宽。""" + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + if size: + font.setPointSize(size) + return font + + +def copy_to_clipboard(text: str, parent: QWidget | None = None) -> bool: + """复制到剪贴板并给出提示;空文本返回 False。""" + if not text: + return False + clipboard = QGuiApplication.clipboard() + if clipboard is None: + return False + clipboard.setText(text) + # 用非阻塞的状态栏提示更优雅,但工具只有几处、用弹窗更直白不易漏看 + if parent is not None: + QMessageBox.information(parent, "已复制", "内容已复制到剪贴板。") + return True + + +def add_copy_button(row: QHBoxLayout, text_provider, label: str = "复制", + parent: QWidget | None = None) -> QPushButton: + """往一行布局里加一个"复制"按钮,内容由 text_provider() 惰性获取。""" + btn = QPushButton(label) + btn.setFixedWidth(72) + btn.clicked.connect(lambda: copy_to_clipboard(text_provider() or "", parent)) + row.addWidget(btn) + return btn + + +def make_mono_label(text: str = "", size: int = 11) -> QLabel: + """等宽、可选中、按内容自适应高度的标签。""" + lbl = QLabel(text) + lbl.setFont(mono_font(size)) + lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + lbl.setWordWrap(True) + lbl.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + return lbl + + +def section(title: str) -> tuple[QGroupBox, QFormLayout]: + """建一个带标题的分组框 + 表单布局,返回 (box, form)。""" + box = QGroupBox(title) + form = QFormLayout(box) + form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + return box, form + + +def hbox(*widgets: QWidget, spacing: int = 8) -> QWidget: + """把若干控件横排进一个容器(用于表单字段位置放"输入框 + 按钮")。""" + container = QWidget() + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(spacing) + for w in widgets: + layout.addWidget(w) + return container + + +def vbox(*layouts_or_widgets) -> QWidget: + """纵向堆叠容器。""" + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + for item in layouts_or_widgets: + if isinstance(item, QWidget): + layout.addWidget(item) + else: + layout.addLayout(item) + return container + + +def warn(parent: QWidget, title: str, text: str) -> None: + QMessageBox.warning(parent, title, text) + + +def info(parent: QWidget, title: str, text: str) -> None: + QMessageBox.information(parent, title, text) + + +def error(parent: QWidget, title: str, text: str) -> None: + QMessageBox.critical(parent, title, text) + + +def ask_overwrite(parent: QWidget, path: Path) -> bool: + """覆盖确认(签发/密钥文件一律先问)。""" + res = QMessageBox.question( + parent, "文件已存在", + f"{path}\n\n已存在,是否覆盖?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + return res == QMessageBox.StandardButton.Yes + + +def discover_plugin_types(root: str | Path) -> list[str]: + """从 ``/config/workers/*/config.toml`` 读出插件类型(失败返回空列表)。 + + 让签发时能勾选"这个项目实际有哪些插件",而不是靠记忆手输。读不到 + (工具不在项目根、或插件目录缺失)时由调用方回退到 FALLBACK_PLUGIN_TYPES。 + """ + base = Path(root) / "config" / "workers" + if not base.is_dir(): + return [] + found: set[str] = set() + for conf in sorted(base.glob("*/config.toml")): + name = conf.parent.name + try: + with conf.open("rb") as f: + data = tomllib.load(f) + wtype = str((data.get("workers") or {}).get("type") or name).strip() + except Exception: # noqa: BLE001 - 单个配置损坏不影响其余插件列出 + wtype = name + if wtype: + found.add(wtype) + return sorted(found) diff --git a/src/chongming_license_gui/inspect_tab.py b/src/chongming_license_gui/inspect_tab.py new file mode 100644 index 0000000..74dcf5e --- /dev/null +++ b/src/chongming_license_gui/inspect_tab.py @@ -0,0 +1,261 @@ +"""查看/校验页:打开 license 文件、用公钥验签、按指定机器模拟校验。 + +"按指定机器校验"是发货前自检的关键:厂商机器自己的指纹当然对不上客户的设备, +若不指定目标机器,只能验出"签名有效"而验不出"机器码是否真的匹配"。 +""" +from __future__ import annotations + +import json +from pathlib import Path + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QButtonGroup, + QFileDialog, + QLabel, + QLineEdit, + QPlainTextEdit, + QPushButton, + QRadioButton, + QVBoxLayout, + QWidget, +) + +from chongming_license import ( + DEFAULT_MIN_MATCH, + RequestCodeError, + decode_request_code, + read_license, +) + +from .common import ( + error, + hbox, + make_mono_label, + section, + warn, +) + +LICENSE_FILTER = "License 文件 (*.lic *.json);;所有文件 (*)" +PUBKEY_FILTER = "公钥文件 (*.key *.pub *.pem);;所有文件 (*)" + + +class InspectTab(QWidget): + """查看/校验页。""" + + def __init__(self, settings, parent=None) -> None: + super().__init__(parent) + self.settings = settings + + root = QVBoxLayout(self) + + # ---------- 选择文件 ---------- + file_box, file_form = section("license 文件") + + self.lic_edit = QLineEdit() + self.lic_edit.setPlaceholderText("选择或填入 license 文件路径") + lic_browse = QPushButton("浏览…") + lic_browse.setFixedWidth(72) + lic_browse.clicked.connect(self._pick_license) + file_form.addRow("文件", hbox(self.lic_edit, lic_browse)) + + self.pub_edit = QLineEdit(str(settings.value("inspect/pubkey", ""))) + self.pub_edit.setPlaceholderText("厂商公钥:base64 裸 32 字节或 PEM(留空则只查看内容、不验签)") + pub_browse = QPushButton("从文件…") + pub_browse.setFixedWidth(88) + pub_browse.clicked.connect(self._pick_pubkey) + file_form.addRow("公钥", hbox(self.pub_edit, pub_browse)) + + root.addWidget(file_box) + + # ---------- 校验对象(指纹比对基准) ---------- + target_box, target_form = section("指纹比对基准") + + self.rb_local = QRadioButton("按本机指纹(在厂商机器上通常对不上,属正常)") + self.rb_remote = QRadioButton("按指定机器(填客户发来的申请码,用于发货前自检)") + self.rb_local.setChecked(True) + group = QButtonGroup(self) + group.addButton(self.rb_local) + group.addButton(self.rb_remote) + + self.rc_edit = QLineEdit() + self.rc_edit.setPlaceholderText("客户设备的申请码 CM1-...") + self.rc_edit.setEnabled(False) + self.rb_remote.toggled.connect(self.rc_edit.setEnabled) + + target_form.addRow("", self.rb_local) + target_form.addRow("", hbox(self.rb_remote, self.rc_edit)) + root.addWidget(target_box) + + # ---------- 结论 ---------- + result_box, result_form = section("校验结论") + + self.verdict = QLabel("尚未校验") + self.verdict.setWordWrap(True) + self.verdict.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + f = self.verdict.font() + f.setBold(True) + self.verdict.setFont(f) + result_form.addRow("结论", self.verdict) + + self.detail = make_mono_label("", size=10) + result_form.addRow("详情", self.detail) + + self.run_btn = QPushButton("校验") + self.run_btn.clicked.connect(self._run) + show_btn = QPushButton("仅查看内容") + show_btn.clicked.connect(self._show_only) + result_form.addRow("", hbox(self.run_btn, show_btn)) + + self.raw_view = QPlainTextEdit() + self.raw_view.setReadOnly(True) + self.raw_view.setFont(make_mono_label().font()) + self.raw_view.setMinimumHeight(220) + self.raw_view.setPlaceholderText("license 内容") + result_form.addRow("内容", self.raw_view) + + root.addWidget(result_box) + root.addStretch(1) + + # ------------------------------------------------------------------ + def _pick_license(self) -> None: + path, _ = QFileDialog.getOpenFileName( + self, "选择 license 文件", self.lic_edit.text() or "", LICENSE_FILTER + ) + if path: + self.lic_edit.setText(path) + self._show_only() + + def _pick_pubkey(self) -> None: + path, _ = QFileDialog.getOpenFileName( + self, "选择公钥文件", self.pub_edit.text() or "", PUBKEY_FILTER + ) + if not path: + return + try: + # 公钥文件按纯文本读:可能是一行 base64,也可能是 PEM + self.pub_edit.setText(Path(path).read_text(encoding="utf-8").strip()) + except Exception as exc: # noqa: BLE001 + error(self, "无法读取公钥", f"{exc}") + return + self.settings.setValue("inspect/pubkey", self.pub_edit.text()) + self._run() + + # ------------------------------------------------------------------ + def _license_path(self) -> Path | None: + raw = self.lic_edit.text().strip() + if not raw: + warn(self, "未选择文件", "请先选择 license 文件。") + return None + path = Path(raw) + if not path.is_file(): + error(self, "文件不存在", f"{path}") + return None + return path + + def _show_only(self) -> None: + """不验签,只把内容显示出来(无公钥时的兜底)。""" + path = self._license_path() + if path is None: + return + try: + doc = json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: # noqa: BLE001 + error(self, "无法解析", f"{exc}") + return + self.raw_view.setPlainText(json.dumps(doc, ensure_ascii=False, indent=2)) + + payload = doc.get("payload") or {} + self.verdict.setText("已载入内容(未验签)") + self.verdict.setStyleSheet("color: #666;") + self.detail.setText( + "\n".join([ + f"授权编号:{payload.get('lic_id', '—')}", + f"客户:{payload.get('customer') or '—'}", + f"机器码:{payload.get('machine_code', '—')}", + f"授权插件:{', '.join(payload.get('plugins') or []) or '—'}", + f"有效期:{payload.get('not_before') or '立即生效'} → " + f"{payload.get('expires_at') or '永久'}", + f"签发时间:{payload.get('issued_at') or '—'}", + f"签名算法:{doc.get('alg', '—')} kid:{doc.get('kid') or '—'}", + "", + "填入公钥后点「校验」可验签并做指纹比对。", + ]) + ) + + def _resolve_sources(self) -> dict | None: + """指纹比对基准:本机(None = 让 read_license 现场采集)或指定申请码。""" + if self.rb_local.isChecked(): + return None + raw = self.rc_edit.text().strip() + if not raw: + raise ValueError("选择了「按指定机器」但未填申请码") + return decode_request_code(raw)["sources"] + + def _run(self) -> None: + path = self._license_path() + if path is None: + return + pub = self.pub_edit.text().strip() + if not pub: + warn(self, "未填公钥", "未提供公钥时无法验签。可点「仅查看内容」先看内容。") + return + + try: + sources = self._resolve_sources() + except (RequestCodeError, ValueError) as exc: + error(self, "申请码无法解析", f"{exc}") + return + + self.settings.setValue("inspect/pubkey", pub) + try: + status = read_license( + path, pub, min_match=DEFAULT_MIN_MATCH, sources=sources + ) + except Exception as exc: # noqa: BLE001 - 公钥格式错误等 + error(self, "校验失败", f"{exc}") + return + + # 内容始终展示,便于对照结论定位问题 + try: + self.raw_view.setPlainText( + json.dumps(json.loads(path.read_text(encoding="utf-8")), + ensure_ascii=False, indent=2) + ) + except Exception: # noqa: BLE001 - 内容展示失败不影响结论 + pass + + if status.ok: + self.verdict.setText(f"✓ {status.message}") + self.verdict.setStyleSheet("color: #2e7d32;") + else: + self.verdict.setText(f"✗ {status.message}") + self.verdict.setStyleSheet("color: #c62828;") + + lines = [ + f"原因码:{status.code}", + f"本机/目标机器码:{status.machine_code}", + ] + if status.match: + lines.append(f"指纹比对:{status.match.reason}") + if status.match.matched_sources: + lines.append(f" 命中来源:{', '.join(status.match.matched_sources)}") + if status.match.mismatched_sources: + lines.append(f" 不匹配来源:{', '.join(status.match.mismatched_sources)}") + if status.match.degraded: + lines.append(" ⚠ 判定已降级(仅 weak 来源可用)") + if status.payload: + p = status.payload + lines += [ + f"授权编号:{p.lic_id}", + f"客户:{p.customer or '—'}", + f"有效期:{p.not_before or '立即生效'} → {p.expires_at or '永久'}", + f"授权插件:{', '.join(sorted(p.plugins_normalized())) or '无'}", + ] + if p.note: + lines.append(f"备注:{p.note}") + if status.public_key_fingerprint: + lines.append(f"公钥指纹:{status.public_key_fingerprint}") + for w in status.warnings: + lines.append(f"⚠ {w}") + self.detail.setText("\n".join(lines)) diff --git a/src/chongming_license_gui/issue_tab.py b/src/chongming_license_gui/issue_tab.py new file mode 100644 index 0000000..27852e8 --- /dev/null +++ b/src/chongming_license_gui/issue_tab.py @@ -0,0 +1,538 @@ +"""签发页:把"申请码 → license 文件"的过程可视化。 + +载荷与签名一律调用 ``chongming_license.issue``(与 CLI 共用),本页只负责 +收集输入、展示解析结果、给出签发前预览 —— 让操作者**在按下签发之前**就能看清 +"给谁、哪台机器、哪些插件、多久",而不是签完才发现勾错了插件。 +""" +from __future__ import annotations + +import json +from pathlib import Path + +from PySide6.QtCore import QDate, QSettings, Qt +from PySide6.QtWidgets import ( + QAbstractItemView, + QButtonGroup, + QCheckBox, + QComboBox, + QDateEdit, + QFileDialog, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QPlainTextEdit, + QPushButton, + QRadioButton, + QSpinBox, + QStackedWidget, + QVBoxLayout, + QWidget, +) + +from chongming_license import ( + RequestCodeError, + build_license, + build_payload, + decode_request_code, + machine_code, + write_license, +) + +from .common import ( + FALLBACK_PLUGIN_TYPES, + ask_overwrite, + copy_to_clipboard, + discover_plugin_types, + error, + hbox, + info, + make_mono_label, + section, + warn, +) +from .keystore import SigningKey + +# 签发依据的三种方式(与 CLI 的 --request-code / --sources-json / --machine-code 对位) +MODE_REQUEST_CODE = 0 +MODE_SOURCES_JSON = 1 +MODE_MACHINE_CODE = 2 + +MODE_LABELS = ("申请码(推荐,支持换硬件容错)", "来源 JSON 文件", "仅机器码(精确匹配,无容错)") + + +class IssueTab(QWidget): + """签发页。""" + + def __init__(self, signing_key: SigningKey, registry, settings: QSettings, + parent=None) -> None: + super().__init__(parent) + self.key = signing_key + self.registry = registry + self.settings = settings + + # 当前解析出的签发对象 + self._sources: dict[str, str] = {} + self._machine_code: str = "" + self._source_note: str = "" + + root = QVBoxLayout(self) + + # ---------- 签名私钥 ---------- + key_box, key_form = section("签名私钥") + self.key_status = QLabel("—") + self.key_status.setWordWrap(True) + self.key_fp = QLabel("—") + self.key_fp.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + key_form.addRow("当前", self.key_status) + key_form.addRow("公钥指纹", self.key_fp) + key_form.addRow("", QLabel("在「密钥」页生成或加载私钥。签发前请核对公钥指纹与设备端一致。")) + root.addWidget(key_box) + + # ---------- 签发依据 ---------- + target_box, target_form = section("签发依据") + + self.mode_combo = QComboBox() + self.mode_combo.addItems(MODE_LABELS) + self.mode_combo.currentIndexChanged.connect(self._on_mode_changed) + target_form.addRow("方式", self.mode_combo) + + self.stack = QStackedWidget() + + # 页 0:申请码 + page_rc = QWidget() + rc_layout = QVBoxLayout(page_rc) + rc_layout.setContentsMargins(0, 0, 0, 0) + self.rc_edit = QPlainTextEdit() + self.rc_edit.setPlaceholderText( + "粘贴客户发来的申请码(CM1-... ,可带换行/空格,会自动清理)" + ) + self.rc_edit.setMaximumHeight(76) + rc_layout.addWidget(self.rc_edit) + rc_btn = QPushButton("解析申请码") + rc_btn.clicked.connect(self._parse_request_code) + rc_layout.addWidget(rc_btn) + self.stack.addWidget(page_rc) + + # 页 1:来源 JSON 文件 + page_json = QWidget() + json_layout = QVBoxLayout(page_json) + json_layout.setContentsMargins(0, 0, 0, 0) + self.json_edit = QLineEdit() + self.json_edit.setPlaceholderText("show --json 的导出文件(含 sources 字段,或就是来源字典)") + json_browse = QPushButton("浏览…") + json_browse.setFixedWidth(72) + json_browse.clicked.connect(self._pick_json) + json_load = QPushButton("解析") + json_load.setFixedWidth(72) + json_load.clicked.connect(self._parse_json_file) + json_layout.addWidget(hbox(self.json_edit, json_browse, json_load)) + self.stack.addWidget(page_json) + + # 页 2:仅机器码 + page_mc = QWidget() + mc_layout = QVBoxLayout(page_mc) + mc_layout.setContentsMargins(0, 0, 0, 0) + self.mc_edit = QLineEdit() + self.mc_edit.setPlaceholderText("如 BC60-D20E-F863-7236-68E2-6A24") + self.mc_edit.setFont(make_mono_label().font()) + mc_btn = QPushButton("使用该机器码") + mc_btn.clicked.connect(self._use_machine_code) + mc_layout.addWidget(hbox(self.mc_edit, mc_btn)) + mc_layout.addWidget(QLabel( + "⚠ 仅按机器码精确匹配:换一块网卡就会失授权。建议改用申请码。" + )) + self.stack.addWidget(page_mc) + + target_form.addRow("", self.stack) + + self.target_result = QLabel("尚未解析签发对象") + self.target_result.setWordWrap(True) + self.target_result.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + target_form.addRow("解析结果", self.target_result) + + self.sources_view = QPlainTextEdit() + self.sources_view.setReadOnly(True) + self.sources_view.setFont(make_mono_label().font()) + self.sources_view.setMaximumHeight(96) + self.sources_view.setPlaceholderText("(各硬件来源指纹,解析成功后显示)") + target_form.addRow("来源指纹", self.sources_view) + + root.addWidget(target_box) + + # ---------- 授权插件 ---------- + plugin_box, plugin_form = section("授权插件") + self.plugin_list = QListWidget() + self.plugin_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.plugin_list.setMaximumHeight(120) + plugin_form.addRow("", self.plugin_list) + + self.plugin_input = QLineEdit() + self.plugin_input.setPlaceholderText("输入插件 type 后点「添加」") + add_btn = QPushButton("添加") + add_btn.setFixedWidth(72) + add_btn.clicked.connect(self._add_plugin) + remove_btn = QPushButton("移除选中") + remove_btn.setFixedWidth(96) + remove_btn.clicked.connect(self._remove_selected_plugins) + all_btn = QPushButton("全选") + all_btn.setFixedWidth(72) + all_btn.clicked.connect(lambda: self._set_all_checked(True)) + none_btn = QPushButton("全不选") + none_btn.setFixedWidth(72) + none_btn.clicked.connect(lambda: self._set_all_checked(False)) + plugin_form.addRow( + "", hbox(self.plugin_input, add_btn, remove_btn, all_btn, none_btn) + ) + + self.root_edit = QLineEdit(str(settings.value("issue/root", str(Path.cwd())))) + self.root_edit.setPlaceholderText("项目根目录(用于自动列出插件)") + reload_btn = QPushButton("重新加载插件列表") + reload_btn.setFixedWidth(140) + reload_btn.clicked.connect(self._reload_plugins) + plugin_form.addRow("项目根", hbox(self.root_edit, reload_btn)) + + root.addWidget(plugin_box) + + # ---------- 授权信息 ---------- + info_box, info_form = section("授权信息") + + self.customer_edit = QLineEdit(str(settings.value("issue/customer", ""))) + info_form.addRow("客户名称", self.customer_edit) + + self.lic_id_edit = QLineEdit("") + self.lic_id_edit.setPlaceholderText("留空则自动生成(L<日期>-<随机>)") + info_form.addRow("授权编号", self.lic_id_edit) + + self.note_edit = QLineEdit(str(settings.value("issue/note", ""))) + self.note_edit.setPlaceholderText("合同号等(仅展示/审计)") + info_form.addRow("备注", self.note_edit) + + self.kid_edit = QLineEdit(str(settings.value("issue/kid", ""))) + self.kid_edit.setPlaceholderText("可选,写入 license 的 kid 字段") + info_form.addRow("密钥标识", self.kid_edit) + + # 有效期三选一 + self.expiry_group = QButtonGroup(self) + self.rb_days = QRadioButton("N 天") + self.rb_days.setChecked(True) + self.days_spin = QSpinBox() + self.days_spin.setRange(1, 36500) + self.days_spin.setValue(int(settings.value("issue/days", 365))) + self.days_spin.setSuffix(" 天") + self.rb_date = QRadioButton("指定到期日") + self.date_edit = QDateEdit() + self.date_edit.setCalendarPopup(True) + self.date_edit.setDisplayFormat("yyyy-MM-dd") + self.date_edit.setDate(QDate.currentDate().addYears(1)) + self.rb_forever = QRadioButton("永久有效") + for rb in (self.rb_days, self.rb_date, self.rb_forever): + self.expiry_group.addButton(rb) + # 启停联动的输入控件,避免"选了永久却还能改天数"的歧义 + self.rb_days.toggled.connect(lambda on: self.days_spin.setEnabled(on)) + self.rb_date.toggled.connect(lambda on: self.date_edit.setEnabled(on)) + self.days_spin.setEnabled(True) + self.date_edit.setEnabled(False) + info_form.addRow("有效期", hbox(self.rb_days, self.days_spin, self.rb_date, self.date_edit)) + info_form.addRow("", self.rb_forever) + + root.addWidget(info_box) + + # ---------- 预览与签发 ---------- + action_box, action_form = section("预览与签发") + self.preview = QPlainTextEdit() + self.preview.setReadOnly(True) + self.preview.setFont(make_mono_label().font()) + self.preview.setMinimumHeight(150) + self.preview.setPlaceholderText("载荷预览:点「刷新预览」") + action_form.addRow("", self.preview) + + refresh_btn = QPushButton("刷新预览") + refresh_btn.clicked.connect(self._refresh_preview) + copy_btn = QPushButton("复制预览") + copy_btn.clicked.connect(lambda: copy_to_clipboard(self.preview.toPlainText(), self)) + self.record_check = QCheckBox("签发后记入台账(便于日后续期与检索)") + self.record_check.setChecked(True) + self.record_check.setToolTip( + "台账会连机器指纹一起存下来,续期时可直接复用,无需再向客户索取申请码" + ) + self.issue_btn = QPushButton("签发并保存…") + self.issue_btn.clicked.connect(self._issue) + action_form.addRow("", hbox(refresh_btn, copy_btn, self.issue_btn)) + action_form.addRow("", self.record_check) + + root.addWidget(action_box) + root.addStretch(1) + + self.key.changed.connect(self._sync_key) + self._sync_key() + self._reload_plugins() + + # ------------------------------------------------------------------ + # 私钥状态 + # ------------------------------------------------------------------ + def _sync_key(self) -> None: + if self.key.loaded: + self.key_status.setText(self.key.describe()) + self.key_fp.setText(self.key.fingerprint) + else: + self.key_status.setText("未加载私钥 —— 请到「密钥」页生成或加载") + self.key_fp.setText("—") + + # ------------------------------------------------------------------ + # 插件列表 + # ------------------------------------------------------------------ + def _reload_plugins(self) -> None: + root = self.root_edit.text().strip() or "." + found = discover_plugin_types(root) + note = "" + if not found: + found = list(FALLBACK_PLUGIN_TYPES) + note = f"(未在 {root}/config/workers 找到插件,已用常见候选)" + + # 保留用户已勾选的项,避免重新加载把选择清掉 + checked = self._checked_plugins() + self.plugin_list.clear() + for ptype in found: + item = QListWidgetItem(ptype) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState( + Qt.CheckState.Checked if ptype in checked else Qt.CheckState.Unchecked + ) + self.plugin_list.addItem(item) + if note: + self.target_result.setText(self.target_result.text()) + self.settings.setValue("issue/root", root) + + def _checked_plugins(self) -> list[str]: + return [ + self.plugin_list.item(i).text() + for i in range(self.plugin_list.count()) + if self.plugin_list.item(i).checkState() == Qt.CheckState.Checked + ] + + def _set_all_checked(self, checked: bool) -> None: + state = Qt.CheckState.Checked if checked else Qt.CheckState.Unchecked + for i in range(self.plugin_list.count()): + self.plugin_list.item(i).setCheckState(state) + + def _add_plugin(self) -> None: + name = self.plugin_input.text().strip() + if not name: + return + existing = {self.plugin_list.item(i).text() for i in range(self.plugin_list.count())} + if name in existing: + warn(self, "已存在", f"{name} 已在列表中。") + self.plugin_input.clear() + return + item = QListWidgetItem(name) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + self.plugin_list.addItem(item) + self.plugin_input.clear() + + def _remove_selected_plugins(self) -> None: + for item in self.plugin_list.selectedItems(): + self.plugin_list.takeItem(self.plugin_list.row(item)) + + # ------------------------------------------------------------------ + # 签发依据解析 + # ------------------------------------------------------------------ + def _on_mode_changed(self, index: int) -> None: + self.stack.setCurrentIndex(index) + self._clear_target() + + def _clear_target(self) -> None: + self._sources = {} + self._machine_code = "" + self._source_note = "" + self.target_result.setText("尚未解析签发对象") + self.sources_view.clear() + self.preview.clear() + + def _pick_json(self) -> None: + path, _ = QFileDialog.getOpenFileName( + self, "选择来源 JSON 文件", self.json_edit.text() or "", "JSON 文件 (*.json);;所有文件 (*)" + ) + if path: + self.json_edit.setText(path) + self._parse_json_file() + + def _parse_request_code(self) -> None: + raw = self.rc_edit.toPlainText() + if not raw.strip(): + warn(self, "未填写", "请粘贴客户发来的申请码。") + return + try: + data = decode_request_code(raw) + except RequestCodeError as exc: + error(self, "申请码无法解析", f"{exc}") + return + self._set_target( + data["sources"], data["machine_code"], + note=f"来自申请码({len(data['sources'])} 个来源)", + ) + + def _parse_json_file(self) -> None: + path = self.json_edit.text().strip() + if not path: + warn(self, "未选择文件", "请选择来源 JSON 文件。") + return + try: + raw = json.loads(Path(path).read_text(encoding="utf-8")) + except Exception as exc: # noqa: BLE001 - 读盘/解析失败统一提示 + error(self, "无法读取", f"{exc}") + return + sources = raw.get("sources", raw) if isinstance(raw, dict) else {} + if not isinstance(sources, dict) or not sources: + error(self, "内容不含来源", f"{path} 中未找到 sources 字段,且本身不是来源字典。") + return + sources = {str(k): str(v) for k, v in sources.items()} + try: + code = raw.get("machine_code") or machine_code(sources) + except Exception as exc: # noqa: BLE001 + error(self, "无法推导机器码", f"{exc}") + return + self._set_target(sources, str(code), note=f"来自文件 {Path(path).name}") + + def _use_machine_code(self) -> None: + code = self.mc_edit.text().strip().upper() + if not code: + warn(self, "未填写", "请输入机器码。") + return + self._set_target({}, code, note="仅机器码(精确匹配,无换硬件容错)") + + def _set_target(self, sources: dict[str, str], code: str, note: str) -> None: + self._sources = dict(sources) + self._machine_code = str(code).strip().upper() + self._source_note = note + + detail = [f"机器码:{self._machine_code}", note] + if sources: + # weak 来源不参与判定阈值,这里如实标注,避免操作者误以为来源越多越保险 + from chongming_license import SOURCES as _SOURCES + + weak = {s.name for s in _SOURCES if s.weak} + strong = sorted(k for k in sources if k not in weak) + weak_found = sorted(k for k in sources if k in weak) + detail.append(f"可信来源 {len(strong)} 个:{', '.join(strong) or '无'}") + if weak_found: + detail.append(f"不参与判定(weak):{', '.join(weak_found)}") + if not strong: + detail.append("⚠ 没有任何可信来源,判定将只依赖易重复的来源,安全性不足") + else: + detail.append("⚠ 无来源指纹:仅按机器码精确匹配,换硬件即失授权") + self.target_result.setText("\n".join(detail)) + + self.sources_view.setPlainText( + "\n".join(f"{k} = {v}" for k, v in sorted(sources.items())) if sources else "(无)" + ) + self._refresh_preview() + + # ------------------------------------------------------------------ + # 预览与签发 + # ------------------------------------------------------------------ + def _expiry_value(self) -> tuple[str | None, int | None]: + """返回 (expires_at, days) —— 二者最多一个非空,供 build_payload/resolve_expiry 使用。""" + if self.rb_forever.isChecked(): + return None, None + if self.rb_days.isChecked(): + return None, self.days_spin.value() + return self.date_edit.date().toString("yyyy-MM-dd"), None + + def _build_payload(self) -> dict: + """收集界面输入并构造载荷(未签名)。""" + if not self._machine_code: + raise ValueError("请先解析签发对象(申请码 / 来源文件 / 机器码)") + plugins = self._checked_plugins() + if not plugins: + raise ValueError("请至少勾选一个授权插件") + + expires_at, days = self._expiry_value() + from chongming_license import resolve_expiry + + return build_payload( + machine_code=self._machine_code, + plugins=plugins, + sources=self._sources, + customer=self.customer_edit.text().strip(), + lic_id=self.lic_id_edit.text().strip(), + expires_at=resolve_expiry(days=days, expires=expires_at), + note=self.note_edit.text().strip(), + ) + + def _refresh_preview(self) -> None: + try: + payload = self._build_payload() + except ValueError as exc: + self.preview.setPlainText(f"(无法生成预览:{exc})") + return + # 预览用与签名完全相同的载荷对象,所见即所签 + self.preview.setPlainText(json.dumps(payload, ensure_ascii=False, indent=2)) + + def _issue(self) -> None: + if not self.key.loaded: + error(self, "未加载私钥", "请先到「密钥」页生成或加载签名私钥。") + return + try: + payload = self._build_payload() + except ValueError as exc: + error(self, "无法签发", str(exc)) + return + + default_dir = self.settings.value("issue/outdir", str(Path.cwd())) + default_name = f"license_{payload['lic_id']}.lic" + path, _ = QFileDialog.getSaveFileName( + self, "保存 license 文件", str(Path(str(default_dir)) / default_name), + "License 文件 (*.lic);;所有文件 (*)", + ) + if not path: + return + target = Path(path) + if target.exists() and not ask_overwrite(self, target): + return + + try: + doc = build_license(payload, self.key.key, kid=self.kid_edit.text().strip()) + write_license(doc, target) + except Exception as exc: # noqa: BLE001 - 签名/写盘失败统一提示 + error(self, "签发失败", f"{exc}") + return + + # 记住这次的选择,便于连续签发给多个客户 + self.settings.setValue("issue/outdir", str(target.parent)) + self.settings.setValue("issue/customer", self.customer_edit.text().strip()) + self.settings.setValue("issue/note", self.note_edit.text().strip()) + self.settings.setValue("issue/kid", self.kid_edit.text().strip()) + self.settings.setValue("issue/days", self.days_spin.value()) + + # 授权编号是自动生成的,签完清空以便下一单重新生成 + self.lic_id_edit.clear() + + # 签发即入库:避免授权文件散落在邮件/下载目录里,日后既不知道给哪台机器 + # 签过,也拿不到那台机器的指纹(续期只能再找客户要申请码) + recorded = "" + if self.record_check.isChecked(): + try: + self.registry.save( + doc, public_key_fp=self.key.fingerprint, source="gui" + ) + recorded = str(self.registry.path) + except Exception as exc: # noqa: BLE001 - 入库失败不应让"已签发"变成失败 + warn(self, "已签发但记入台账失败", + f"{exc}\n\n授权文件已正常生成,只是台账未记录。" + "请到「台账」页检查台账库路径是否可写。") + + info( + self, "已签发", + f"{target}\n\n" + f"授权编号:{payload['lic_id']}\n" + f"客户:{payload['customer'] or '(未填)'}\n" + f"机器码:{payload['machine_code']}\n" + f"授权插件:{', '.join(payload['plugins'])}\n" + f"有效期:{payload['not_before'] or '立即生效'} → {payload['expires_at'] or '永久'}\n" + f"公钥指纹:{self.key.fingerprint}\n\n" + + (f"已记入台账:{recorded}\n" + f"续期可用「台账」页,无需客户再提供申请码。\n\n" if recorded else "") + + "把该文件交给客户,在设备端「授权」页导入后重启网关生效。", + ) diff --git a/src/chongming_license_gui/keys_tab.py b/src/chongming_license_gui/keys_tab.py new file mode 100644 index 0000000..96d3b7b --- /dev/null +++ b/src/chongming_license_gui/keys_tab.py @@ -0,0 +1,225 @@ +"""密钥页:生成密钥对 / 加载现有私钥 / 查看对应公钥。 + +一键生成后立即把公钥填进剪贴板 —— 下一步(注入构建)就要用,少一次手工复制 +就少一次抄错。 +""" +from __future__ import annotations + +from pathlib import Path + +from PySide6.QtCore import QSettings, Qt +from PySide6.QtWidgets import ( + QFileDialog, + QLabel, + QLineEdit, + QPlainTextEdit, + QPushButton, + QVBoxLayout, + QWidget, +) + +from chongming_license import LicenseError, generate_keypair, write_private_key + +from .common import ( + copy_to_clipboard, + error, + info, + make_mono_label, + section, + warn, +) +from .keystore import SigningKey + +PRIVATE_KEY_FILTER = "私钥文件 (*.pem *.key);;所有文件 (*)" + + +class KeysTab(QWidget): + """密钥管理页。""" + + def __init__(self, signing_key: SigningKey, settings: QSettings, parent=None) -> None: + super().__init__(parent) + self.key = signing_key + self.settings = settings + + root = QVBoxLayout(self) + + # ---------- 生成密钥对 ---------- + gen_box, gen_form = section("生成密钥对(厂商侧,一次性)") + + self.out_edit = QLineEdit(str(settings.value("keys/output", "license_private.pem"))) + browse_out = QPushButton("浏览…") + browse_out.setFixedWidth(72) + browse_out.clicked.connect(self._pick_out) + gen_form.addRow("私钥保存到", self._row(self.out_edit, browse_out)) + + self.kid_edit = QLineEdit(str(settings.value("keys/kid", ""))) + self.kid_edit.setPlaceholderText("可选,写入 license 的 kid 字段(如 vendor-2026)") + gen_form.addRow("密钥标识", self.kid_edit) + + gen_btn = QPushButton("生成密钥对") + gen_btn.clicked.connect(self._generate) + gen_form.addRow("", gen_btn) + + self.pub_label = make_mono_label("(尚未生成)", size=10) + gen_form.addRow("公钥 base64", self._row(self.pub_label, self._copy_btn(lambda: self.key.public_b64))) + + self.fp_label = QLabel("—") + self.fp_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + gen_form.addRow("公钥指纹", self.fp_label) + + self.pem_view = QPlainTextEdit() + self.pem_view.setReadOnly(True) + self.pem_view.setFont(make_mono_label().font()) + self.pem_view.setMaximumHeight(110) + self.pem_view.setPlaceholderText("(公钥 PEM,可选使用)") + gen_form.addRow("公钥 PEM", self.pem_view) + gen_form.addRow("", self._copy_btn(lambda: self.key.public_pem(), "复制 PEM")) + + caution = QLabel( + "⚠ 私钥是授权的唯一信任根:请离线保管并做好备份,切勿提交仓库或打进部署产物。\n" + "私钥丢失 = 无法再签发新授权;私钥泄漏 = 任何人都能签发授权。\n" + "内置防护:.gitignore 忽略私钥文件;`uv run build` 会扫描产物并在发现私钥时终止构建。" + ) + caution.setWordWrap(True) + caution.setStyleSheet("color: #b8860b;") + gen_form.addRow("", caution) + + root.addWidget(gen_box) + + # ---------- 加载现有私钥 ---------- + load_box, load_form = section("加载现有私钥") + + self.in_edit = QLineEdit(str(settings.value("keys/input", ""))) + self.in_edit.setPlaceholderText("已有私钥的路径(用于签发或核对公钥)") + browse_in = QPushButton("浏览…") + browse_in.setFixedWidth(72) + browse_in.clicked.connect(self._pick_in) + load_btn = QPushButton("加载") + load_btn.setFixedWidth(72) + load_btn.clicked.connect(self._load) + load_form.addRow("私钥路径", self._row(self.in_edit, browse_in, load_btn)) + + self.load_status = QLabel("—") + self.load_status.setWordWrap(True) + self.load_status.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + load_form.addRow("状态", self.load_status) + + root.addWidget(load_box) + root.addStretch(1) + + # 外部(签发页)改动了私钥时同步显示 + self.key.changed.connect(self._sync_from_state) + self._sync_from_state() + + # 启动时若记住了路径就尝试自动加载,省一步点击 + if self.in_edit.text().strip(): + self._load(silent=True) + + # ------------------------------------------------------------------ + @staticmethod + def _row(*widgets: QWidget) -> QWidget: + from .common import hbox + + return hbox(*widgets) + + @staticmethod + def _copy_btn(provider, label: str = "复制") -> QPushButton: + """独立成行的复制按钮(不依赖某个父布局)。""" + btn = QPushButton(label) + btn.setFixedWidth(96) + btn.clicked.connect(lambda: copy_to_clipboard(provider() or "", None)) + return btn + + def _pick_out(self) -> None: + path, _ = QFileDialog.getSaveFileName( + self, "私钥保存位置", self.out_edit.text() or "license_private.pem", + PRIVATE_KEY_FILTER, + ) + if path: + self.out_edit.setText(path) + + def _pick_in(self) -> None: + path, _ = QFileDialog.getOpenFileName( + self, "选择私钥文件", self.in_edit.text() or "", PRIVATE_KEY_FILTER + ) + if path: + self.in_edit.setText(path) + self._load() + + # ------------------------------------------------------------------ + def _generate(self) -> None: + out = Path(self.out_edit.text().strip() or "license_private.pem") + if out.exists() and not self._confirm_overwrite(out): + return + try: + key, pub = generate_keypair() + write_private_key(key, out, overwrite=True) + except Exception as exc: # noqa: BLE001 - 写盘失败原因多样,统一弹窗 + error(self, "生成失败", f"{exc}") + return + + self.settings.setValue("keys/output", str(out)) + self.key.set_key(key, out) + # 生成后顺手把公钥放进剪贴板:下一步注入构建就要用它 + copy_to_clipboard(pub, None) + info( + self, "已生成密钥对", + f"私钥:{out}(权限 0600)\n\n" + f"公钥(已复制到剪贴板):\n{pub}\n\n" + f"公钥指纹:{self.key.fingerprint}\n\n" + "请把公钥注入构建产物(CHONGMING_LICENSE_PUBKEY 环境变量或 license_public.key 文件),\n" + "并把私钥离线备份。", + ) + + def _confirm_overwrite(self, path: Path) -> bool: + from .common import ask_overwrite + + return ask_overwrite(self, path) + + def _load(self, _checked: bool = False, silent: bool = False) -> None: + raw = self.in_edit.text().strip() + if not raw: + if not silent: + warn(self, "未填写路径", "请先选择私钥文件。") + return + try: + pub = self.key.load(raw) + except LicenseError as exc: + self.load_status.setText(f"✗ {exc}") + if not silent: + error(self, "加载失败", str(exc)) + return + except Exception as exc: # noqa: BLE001 - 非 PEM / 权限问题等 + self.load_status.setText(f"✗ 无法读取:{exc}") + if not silent: + error(self, "加载失败", f"{exc}") + return + + self.settings.setValue("keys/input", raw) + self.load_status.setText(f"✓ 已加载,公钥 = {pub}") + if not silent: + info( + self, "已加载私钥", + f"{raw}\n\n公钥:\n{pub}\n\n公钥指纹:{self.key.fingerprint}\n\n" + "请确认该指纹与设备端显示的一致 —— 不一致说明用的不是同一把密钥。", + ) + + def _sync_from_state(self) -> None: + """与共享状态保持一致(签发页加载私钥后这里也要变)。""" + if self.key.loaded: + self.pub_label.setText(self.key.public_b64) + self.fp_label.setText(self.key.fingerprint) + self.pem_view.setPlainText(self.key.public_pem()) + if self.key.path is not None: + # 路径栏同步,方便用户看到当前生效的是哪个文件 + if self.in_edit.text().strip() != str(self.key.path): + self.in_edit.setText(str(self.key.path)) + self.load_status.setText( + f"✓ 当前生效:{self.key.path}(公钥指纹 {self.key.fingerprint})" + ) + elif not self.in_edit.text().strip(): + self.load_status.setText(f"✓ 已加载(公钥指纹 {self.key.fingerprint})") + else: + self.pub_label.setText("(尚未加载私钥)") + self.fp_label.setText("—") + self.pem_view.clear() diff --git a/src/chongming_license_gui/keystore.py b/src/chongming_license_gui/keystore.py new file mode 100644 index 0000000..ca2ba60 --- /dev/null +++ b/src/chongming_license_gui/keystore.py @@ -0,0 +1,96 @@ +"""当前签名私钥的共享状态(跨 Tab 同步)。 + +GUI 里私钥在两处被用到:密钥页(生成/加载/查看)与签发页(实际签名)。若各自 +维护一份路径,会出现"密钥页换了私钥、签发页还在用旧的"这类危险的不一致 —— +签出来的授权对应哪把公钥就说不清了。因此收敛成一个带信号的状态对象。 + +私钥对象本身只存在内存中,不落任何配置;``QSettings`` 里只记住**路径**。 +""" +from __future__ import annotations + +from pathlib import Path + +from PySide6.QtCore import QObject, Signal + +from chongming_license import ( + LicenseError, + load_private_key, + public_key_b64, + public_key_fingerprint, + public_key_pem, +) + + +class SigningKey(QObject): + """当前用于签发的私钥。""" + + changed = Signal() + + def __init__(self) -> None: + super().__init__() + self._path: Path | None = None + self._key = None + self._public_b64: str = "" + + # ---- 读 ---- + @property + def loaded(self) -> bool: + return self._key is not None + + @property + def path(self) -> Path | None: + return self._path + + @property + def key(self): + """Ed25519PrivateKey;未加载时为 None。""" + return self._key + + @property + def public_b64(self) -> str: + """公钥 base64(可直接填进 config.toml 的 [license] public_key)。""" + return self._public_b64 + + @property + def fingerprint(self) -> str: + return public_key_fingerprint(self._public_b64) if self._public_b64 else "" + + def public_pem(self) -> str: + return public_key_pem(self._key) if self._key is not None else "" + + # ---- 写 ---- + def load(self, path: str | Path) -> str: + """加载私钥;返回公钥 base64。失败抛 :class:`LicenseError`。 + + 加载成功后**立即**读出公钥与指纹:这是发现"拿错私钥"的唯一时机 —— + 签完再发现就得重签并追回已发出的文件。 + """ + target = Path(path) + if not target.is_file(): + raise LicenseError(f"私钥文件不存在:{target}") + key = load_private_key(target) + self._key = key + self._path = target + self._public_b64 = public_key_b64(key) + self.changed.emit() + return self._public_b64 + + def set_key(self, key, path: str | Path | None = None) -> str: + """直接设置私钥对象(生成密钥对后调用)。""" + self._key = key + self._path = Path(path) if path else None + self._public_b64 = public_key_b64(key) + self.changed.emit() + return self._public_b64 + + def clear(self) -> None: + self._key = None + self._path = None + self._public_b64 = "" + self.changed.emit() + + def describe(self) -> str: + """一行状态描述(供界面显示)。""" + if not self.loaded: + return "未加载私钥 —— 请到「密钥」页生成或加载" + return f"{self._path}(公钥指纹 {self.fingerprint})" diff --git a/src/chongming_license_gui/registry_store.py b/src/chongming_license_gui/registry_store.py new file mode 100644 index 0000000..ea5e9af --- /dev/null +++ b/src/chongming_license_gui/registry_store.py @@ -0,0 +1,87 @@ +"""台账库的共享状态(跨 Tab 同步)。 + +与 :mod:`keystore` 同样的理由:台账被两处用到 —— 签发页(签发即入库)与台账页 +(检索/续期/导出)。若各自持有一个连接,"签发页写进去的记录在台账页看不到" +(需要手动刷新)或两边指向不同文件,都会让人以为丢了授权。 + +未显式打开时按"签发即入库"的原则自动打开默认库:宁可多一个文件, +也不要让签出去的授权因为"忘了记"而从此查不到。 +""" +from __future__ import annotations + +from pathlib import Path + +from PySide6.QtCore import QObject, Signal + +from chongming_license import ( + LicenseRecord, + Registry, + RegistryError, + default_registry_path, +) + + +class RegistryHolder(QObject): + """当前台账库。""" + + changed = Signal() + + def __init__(self) -> None: + super().__init__() + self._registry: Registry | None = None + + # ---- 状态 ---- + @property + def is_open(self) -> bool: + return self._registry is not None + + @property + def path(self) -> Path | None: + return self._registry.path if self._registry else None + + @property + def registry(self) -> Registry | None: + return self._registry + + # ---- 操作 ---- + def open(self, path: str | Path) -> Registry: + """打开(或新建)台账库;失败抛 :class:`RegistryError`。 + + 切换路径时先关掉旧连接,避免句柄泄漏。 + """ + target = Path(path).expanduser() + if self._registry is not None and self._registry.path == target: + return self._registry + new_reg = Registry(target) # 先构造,失败则保留旧连接 + if self._registry is not None: + self._registry.close() + self._registry = new_reg + self.changed.emit() + return new_reg + + def ensure_open(self) -> Registry: + """确保可用:未打开时打开默认库(签发页"签发即入库"用)。""" + if self._registry is None: + return self.open(default_registry_path()) + return self._registry + + def close(self) -> None: + if self._registry is not None: + self._registry.close() + self._registry = None + self.changed.emit() + + # ---- 便捷透传(供签发页调用) ---- + def save(self, license_doc, *, public_key_fp: str = "", source: str = "") -> LicenseRecord: + return self.ensure_open().save( + license_doc, public_key_fp=public_key_fp, source=source + ) + + def find_by_machine_code(self, machine_code: str) -> list[LicenseRecord]: + """某台机器已有的授权(续期前先看一眼上次签了什么)。""" + if self._registry is None or not machine_code: + return [] + return self._registry.find_by_machine_code(machine_code) + + +__all__ = ["RegistryHolder", "RegistryError", "default_registry_path"] diff --git a/src/chongming_license_gui/registry_tab.py b/src/chongming_license_gui/registry_tab.py new file mode 100644 index 0000000..8792a6d --- /dev/null +++ b/src/chongming_license_gui/registry_tab.py @@ -0,0 +1,649 @@ +"""台账页:检索已签发的授权、续期、编辑备注、导出归档。 + +台账最大的价值是**续期**:现场设备要续期时常常联系不上客户,而机器指纹台账里 +已经有了 —— 直接复用重签,不必再索取申请码。所以"续期"是本页的主操作。 +""" +from __future__ import annotations + +import json +from pathlib import Path + +from PySide6.QtCore import QDate, QSettings, Qt +from PySide6.QtWidgets import ( + QAbstractItemView, + QButtonGroup, + QComboBox, + QDateEdit, + QDialog, + QDialogButtonBox, + QFileDialog, + QInputDialog, + QLabel, + QLineEdit, + QPlainTextEdit, + QPushButton, + QRadioButton, + QSpinBox, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + +from chongming_license import ( + DEFAULT_EXPIRING_DAYS, + LicenseRecord, + default_registry_path, + resolve_expiry, +) + +from .common import ( + ask_overwrite, + copy_to_clipboard, + error, + hbox, + info, + make_mono_label, + section, + warn, +) +from .keystore import SigningKey +from .registry_store import RegistryHolder + +STATUS_CHOICES = [ + ("", "全部状态"), + ("expiring", "即将到期"), + ("expired", "已过期"), + ("active", "有效"), + ("forever", "永久有效"), + ("not_yet_valid", "未生效"), +] + +# 状态 -> 前景色(表格里一眼分辨急缓) +STATUS_COLORS = { + "expired": "#c62828", + "expiring": "#e65100", + "active": "#2e7d32", + "forever": "#1565c0", + "not_yet_valid": "#6a1b9a", +} + + +class RenewDialog(QDialog): + """续期对话框:预填原授权信息,只让人改"要变的部分"。""" + + def __init__(self, record: LicenseRecord, parent=None) -> None: + super().__init__(parent) + self.record = record + self.setWindowTitle(f"续期 {record.lic_id}") + self.setMinimumWidth(560) + + root = QVBoxLayout(self) + + # 原授权信息(只读,避免操作者误以为改这里能改历史记录) + src_box, src_form = section("原授权") + src_form.addRow("授权编号", QLabel(record.lic_id)) + src_form.addRow("客户", QLabel(record.customer or "(未填)")) + src_form.addRow("机器码", make_mono_label(record.machine_code, size=10)) + src_form.addRow("授权插件", QLabel(", ".join(record.plugins) or "(无)")) + src_form.addRow( + "有效期", + QLabel(f"{record.not_before or '立即生效'} → " + f"{record.expires_at or '永久'}({record.status_label()})"), + ) + src_form.addRow( + "指纹来源", + QLabel(f"{len(record.sources)} 个:{', '.join(sorted(record.sources)) or '无'}\n" + "✓ 续期直接复用这些指纹,无需向客户索取申请码"), + ) + root.addWidget(src_box) + + # 新授权 + new_box, new_form = section("新授权") + + self.rb_days = QRadioButton("N 天") + self.rb_days.setChecked(True) + self.days_spin = QSpinBox() + self.days_spin.setRange(1, 36500) + self.days_spin.setValue(365) + self.days_spin.setSuffix(" 天") + self.rb_date = QRadioButton("指定到期日") + self.date_edit = QDateEdit() + self.date_edit.setCalendarPopup(True) + self.date_edit.setDisplayFormat("yyyy-MM-dd") + self.date_edit.setDate(QDate.currentDate().addYears(1)) + self.date_edit.setEnabled(False) + self.rb_forever = QRadioButton("永久有效") + group = QButtonGroup(self) + for rb in (self.rb_days, self.rb_date, self.rb_forever): + group.addButton(rb) + self.rb_days.toggled.connect(self.days_spin.setEnabled) + self.rb_date.toggled.connect(self.date_edit.setEnabled) + new_form.addRow("有效期", hbox(self.rb_days, self.days_spin, + self.rb_date, self.date_edit)) + new_form.addRow("", self.rb_forever) + + self.plugins_edit = QLineEdit(",".join(record.plugins)) + self.plugins_edit.setPlaceholderText("逗号分隔;留空表示沿用原白名单") + new_form.addRow("授权插件", self.plugins_edit) + + self.customer_edit = QLineEdit(record.customer) + new_form.addRow("客户", self.customer_edit) + + self.note_edit = QLineEdit(record.note) + new_form.addRow("备注", self.note_edit) + + # 属性名按"选项语义"命名(rb_* 后面跟的是该选项的语义),不要跟标签反着来 + self.rb_new_id = QRadioButton("生成新授权编号(推荐:新旧都留台账,可追溯)") + self.rb_new_id.setChecked(True) + self.rb_keep_id = QRadioButton("沿用原授权编号") + g2 = QButtonGroup(self) + g2.addButton(self.rb_new_id) + g2.addButton(self.rb_keep_id) + new_form.addRow("编号", self.rb_new_id) + new_form.addRow("", self.rb_keep_id) + + root.addWidget(new_box) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.button(QDialogButtonBox.StandardButton.Ok).setText("续期并保存…") + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + root.addWidget(buttons) + + def values(self) -> dict: + if self.rb_forever.isChecked(): + days, expires = None, None + elif self.rb_days.isChecked(): + days, expires = self.days_spin.value(), None + else: + days, expires = None, self.date_edit.date().toString("yyyy-MM-dd") + plugins = [p.strip() for p in self.plugins_edit.text().split(",") if p.strip()] + return { + "expires_at": resolve_expiry(days=days, expires=expires), + "plugins": plugins or list(self.record.plugins), + "customer": self.customer_edit.text().strip(), + "note": self.note_edit.text().strip(), + "keep_lic_id": self.rb_keep_id.isChecked(), + } + + +class RegistryTab(QWidget): + """台账页。""" + + def __init__(self, signing_key: SigningKey, holder: RegistryHolder, + settings: QSettings, parent=None) -> None: + super().__init__(parent) + self.key = signing_key + self.holder = holder + self.settings = settings + self.records: list[LicenseRecord] = [] + + root = QVBoxLayout(self) + + # ---------- 台账库 ---------- + db_box, db_form = section("台账库") + self.db_edit = QLineEdit( + str(settings.value("registry/path", str(default_registry_path()))) + ) + open_btn = QPushButton("打开 / 新建") + open_btn.setFixedWidth(110) + open_btn.clicked.connect(self._open_db) + browse_btn = QPushButton("浏览…") + browse_btn.setFixedWidth(72) + browse_btn.clicked.connect(self._pick_db) + db_form.addRow("路径", hbox(self.db_edit, browse_btn, open_btn)) + + self.stats_label = QLabel("未打开台账库") + self.stats_label.setWordWrap(True) + db_form.addRow("概览", self.stats_label) + + self.key_label = QLabel("—") + self.key_label.setWordWrap(True) + db_form.addRow("签名私钥", self.key_label) + root.addWidget(db_box) + + # ---------- 筛选 ---------- + filter_box, filter_form = section("筛选") + + self.customer_edit = QLineEdit() + self.customer_edit.setPlaceholderText("客户名(模糊)") + self.machine_edit = QLineEdit() + self.machine_edit.setPlaceholderText("机器码(模糊,大小写不敏感)") + self.plugin_edit = QLineEdit() + self.plugin_edit.setPlaceholderText("插件 type(精确)") + self.keyword_edit = QLineEdit() + self.keyword_edit.setPlaceholderText("关键字:编号/机器码/客户/备注") + filter_form.addRow("客户", hbox(self.customer_edit, QLabel("机器码"), self.machine_edit)) + filter_form.addRow("插件", hbox(self.plugin_edit, QLabel("关键字"), self.keyword_edit)) + + self.status_combo = QComboBox() + for value, label in STATUS_CHOICES: + self.status_combo.addItem(label, value) + self.days_spin = QSpinBox() + self.days_spin.setRange(1, 3650) + self.days_spin.setValue(DEFAULT_EXPIRING_DAYS) + self.days_spin.setSuffix(" 天内") + self.days_spin.setToolTip("「即将到期」的判定窗口") + filter_form.addRow("状态", hbox(self.status_combo, self.days_spin)) + + query_btn = QPushButton("查询") + query_btn.clicked.connect(self.refresh) + reset_btn = QPushButton("重置") + reset_btn.clicked.connect(self._reset_filters) + short_btn = QPushButton("只看即将到期") + short_btn.clicked.connect(self._filter_expiring) + filter_form.addRow("", hbox(query_btn, reset_btn, short_btn)) + root.addWidget(filter_box) + + # ---------- 列表 ---------- + table_box, table_form = section("已签发授权(按到期日升序,最急的在前)") + self.table = QTableWidget(0, 9) + self.table.setHorizontalHeaderLabels([ + "授权编号", "客户", "机器码", "授权插件", "状态", "到期", "剩余", "备注", "公钥指纹", + ]) + self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.verticalHeader().setVisible(False) + self.table.setMinimumHeight(260) + widths = [170, 130, 250, 200, 110, 100, 70, 180, 110] + for i, w in enumerate(widths): + self.table.setColumnWidth(i, w) + table_form.addRow("", self.table) + + renew_btn = QPushButton("续期选中…") + renew_btn.clicked.connect(self._renew) + note_btn = QPushButton("编辑备注") + note_btn.clicked.connect(self._edit_note) + copy_btn = QPushButton("复制机器码") + copy_btn.clicked.connect(self._copy_machine_code) + detail_btn = QPushButton("查看详情") + detail_btn.clicked.connect(self._show_detail) + del_btn = QPushButton("删除记录") + del_btn.clicked.connect(self._delete) + table_form.addRow("", hbox(renew_btn, note_btn, copy_btn, detail_btn, del_btn)) + root.addWidget(table_box) + + # ---------- 导出 ---------- + exp_box, exp_form = section("导出归档") + self.out_edit = QLineEdit(str(settings.value("registry/outdir", ""))) + self.out_edit.setPlaceholderText("导出目录(导出当前筛选结果)") + out_browse = QPushButton("浏览…") + out_browse.setFixedWidth(72) + out_browse.clicked.connect(self._pick_outdir) + exp_form.addRow("目录", hbox(self.out_edit, out_browse)) + + self.by_customer = QComboBox() + self.by_customer.addItem("平铺", False) + self.by_customer.addItem("按客户分子目录", True) + exp_form.addRow("组织方式", self.by_customer) + + lic_btn = QPushButton("导出 .lic") + lic_btn.clicked.connect(self._export_licenses) + csv_btn = QPushButton("导出 CSV 清单") + csv_btn.clicked.connect(self._export_csv) + exp_form.addRow("", hbox(lic_btn, csv_btn)) + exp_form.addRow("", QLabel( + "CSV 以 UTF-8 BOM 写入,Excel 双击打开中文不乱码;字段可含逗号(已按 CSV 规范转义)。" + )) + root.addWidget(exp_box) + root.addStretch(1) + + self.holder.changed.connect(self._sync_holder) + self.key.changed.connect(self._sync_key) + self._sync_key() + self._sync_holder() + + # 默认路径已存在就直接打开,省一步点击 + if Path(self.db_edit.text()).expanduser().is_file(): + self._open_db(silent=True) + + # ------------------------------------------------------------------ + # 状态同步 + # ------------------------------------------------------------------ + def _sync_key(self) -> None: + if self.key.loaded: + self.key_label.setText(f"{self.key.path}(公钥指纹 {self.key.fingerprint})") + else: + self.key_label.setText( + "未加载私钥 —— 续期需要签名私钥,请到「密钥」页生成或加载" + ) + + def _sync_holder(self) -> None: + if self.holder.is_open: + self.stats_label.setText(f"已打开:{self.holder.path}(点「查询」刷新)") + else: + self.stats_label.setText("未打开台账库") + + # ------------------------------------------------------------------ + # 台账库 + # ------------------------------------------------------------------ + def _pick_db(self) -> None: + path, _ = QFileDialog.getSaveFileName( + self, "选择或新建台账库", self.db_edit.text() or str(default_registry_path()), + "SQLite 数据库 (*.db);;所有文件 (*)", + ) + if path: + self.db_edit.setText(path) + self._open_db() + + def _open_db(self, _checked: bool = False, silent: bool = False) -> None: + raw = self.db_edit.text().strip() + if not raw: + if not silent: + warn(self, "未填路径", "请填写台账库路径。") + return + try: + self.holder.open(raw) + except Exception as exc: # noqa: BLE001 - 路径/权限/结构版本都可能失败 + self.stats_label.setText(f"✗ 打开失败:{exc}") + if not silent: + error(self, "打开台账库失败", f"{exc}") + return + self.settings.setValue("registry/path", raw) + # 首次新建的库是空的,提示一下避免以为"数据没了"。 + # 注意这里**不能**用 `with self.holder.registry:` —— Registry 的 __exit__ + # 会关闭连接,而这条连接由 holder 长期持有(with 是"用完即关"语义)。 + try: + st = self.holder.registry.stats(expiring_days=self.days_spin.value()) + except Exception as exc: # noqa: BLE001 + error(self, "读取台账失败", f"{exc}") + return + if st["total"] == 0 and not silent: + info(self, "台账库已就绪", + f"{self.holder.path}\n\n当前是空台账。之后每次签发都会自动入库。") + self.refresh() + + # ------------------------------------------------------------------ + # 查询 + # ------------------------------------------------------------------ + def _reset_filters(self) -> None: + self.customer_edit.clear() + self.machine_edit.clear() + self.plugin_edit.clear() + self.keyword_edit.clear() + self.status_combo.setCurrentIndex(0) + self.days_spin.setValue(DEFAULT_EXPIRING_DAYS) + self.refresh() + + def _filter_expiring(self) -> None: + self.status_combo.setCurrentIndex(1) # 即将到期 + self.refresh() + + def refresh(self) -> None: + if self.holder.registry is None: + self._open_db(silent=True) + reg = self.holder.registry + if reg is None: + return + + days = self.days_spin.value() + try: + self.records = reg.query( + customer=self.customer_edit.text().strip() or None, + machine_code=self.machine_edit.text().strip() or None, + plugin=self.plugin_edit.text().strip() or None, + status=self.status_combo.currentData() or None, + keyword=self.keyword_edit.text().strip() or None, + expiring_days=days, + ) + st = reg.stats(expiring_days=days) + except Exception as exc: # noqa: BLE001 + error(self, "查询失败", f"{exc}") + return + + self.stats_label.setText( + f"{reg.path}\n" + f"共 {st['total']} 条 —— 有效 {st['active']}、即将到期 {st['expiring']}、" + f"已过期 {st['expired']}、永久 {st['forever']}、未生效 {st['not_yet_valid']}" + f";当前筛选出 {len(self.records)} 条" + ) + self._fill_table() + + def _fill_table(self) -> None: + self.table.setRowCount(len(self.records)) + for row, rec in enumerate(self.records): + left = rec.days_left() + status = rec.status_label() + cells = [ + rec.lic_id, + rec.customer or "(未填)", + rec.machine_code, + ", ".join(rec.plugins), + status + (f" ({left}d)" if left is not None else ""), + rec.expires_at or "永久", + "" if left is None else str(left), + rec.note, + rec.public_key_fp, + ] + for col, text in enumerate(cells): + item = QTableWidgetItem(text) + if col in (4,): + color = STATUS_COLORS.get(rec.status(), "#666666") + item.setForeground(Qt.GlobalColor.black) + from PySide6.QtGui import QColor + + item.setForeground(QColor(color)) + self.table.setItem(row, col, item) + + def _selected_records(self) -> list[LicenseRecord]: + rows = sorted({i.row() for i in self.table.selectedIndexes()}) + return [self.records[i] for i in rows if 0 <= i < len(self.records)] + + # ------------------------------------------------------------------ + # 单条操作 + # ------------------------------------------------------------------ + def _renew(self) -> None: + selected = self._selected_records() + if not selected: + warn(self, "未选中", "请先在列表里选中一条要续期的授权。") + return + if len(selected) > 1: + warn(self, "一次一条", "续期请只选中一条(需要确认有效期与插件)。") + return + if not self.key.loaded: + error(self, "未加载私钥", "续期需要签名私钥,请先到「密钥」页加载。") + return + + rec = selected[0] + dlg = RenewDialog(rec, self) + if dlg.exec() != QDialog.DialogCode.Accepted: + return + vals = dlg.values() + + default_dir = self.settings.value("registry/outdir", str(Path.cwd())) + out_path, _ = QFileDialog.getSaveFileName( + self, "保存续期后的 license", + str(Path(str(default_dir)) / f"license_{rec.lic_id}.lic"), + "License 文件 (*.lic);;所有文件 (*)", + ) + if not out_path: + return + target = Path(out_path) + if target.exists() and not ask_overwrite(self, target): + return + + from chongming_license import build_license, build_payload, write_license + + try: + payload = build_payload( + machine_code=rec.machine_code, + plugins=vals["plugins"], + sources=rec.sources, # 复用台账指纹:续期的关键 + customer=vals["customer"], + note=vals["note"], + expires_at=vals["expires_at"], + ) + if vals["keep_lic_id"]: + payload["lic_id"] = rec.lic_id + doc = build_license(payload, self.key.key, kid=rec.kid) + write_license(doc, target) + + reg = self.holder.ensure_open() + new_rec = reg.save(doc, public_key_fp=self.key.fingerprint, source="renew") + if not vals["keep_lic_id"]: + link = f"已续期 → {new_rec.lic_id}" + if link not in rec.note: + reg.update_note(rec.lic_id, (rec.note + ";" if rec.note else "") + link) + except Exception as exc: # noqa: BLE001 + error(self, "续期失败", f"{exc}") + return + + self.settings.setValue("registry/outdir", str(target.parent)) + self.refresh() + info( + self, "已续期", + f"{target}\n\n" + f"新授权:{new_rec.lic_id}(到期 {new_rec.expires_at or '永久'})\n" + f"指纹来源:复用台账里的 {len(rec.sources)} 个来源," + "**无需客户再提供申请码**\n\n" + "把新 .lic 交给客户,在设备端「授权」页导入后重启网关生效。", + ) + + def _edit_note(self) -> None: + selected = self._selected_records() + if len(selected) != 1: + warn(self, "未选中", "请选中一条记录。") + return + rec = selected[0] + text, ok = QInputDialog.getText( + self, f"编辑备注 {rec.lic_id}", + "台账备注(人工批注,重新导入 license 不会被覆盖):", + QLineEdit.EchoMode.Normal, rec.note, + ) + if not ok: + return + self.holder.ensure_open().update_note(rec.lic_id, text) + self.refresh() + + def _copy_machine_code(self) -> None: + selected = self._selected_records() + if len(selected) != 1: + warn(self, "未选中", "请选中一条记录。") + return + copy_to_clipboard(selected[0].machine_code, self) + + def _show_detail(self) -> None: + selected = self._selected_records() + if len(selected) != 1: + warn(self, "未选中", "请选中一条记录。") + return + rec = selected[0] + doc = rec.license_doc() + dlg = QDialog(self) + dlg.setWindowTitle(f"授权详情 {rec.lic_id}") + dlg.resize(760, 620) + lay = QVBoxLayout(dlg) + + summary = QPlainTextEdit() + summary.setReadOnly(True) + summary.setFont(make_mono_label().font()) + left = rec.days_left() + summary.setPlainText("\n".join([ + f"授权编号:{rec.lic_id}", + f"客户:{rec.customer or '(未填)'}", + f"机器码:{rec.machine_code}", + f"授权插件:{', '.join(rec.plugins) or '(无)'}", + f"状态:{rec.status_label()}" + (f"(剩余 {left} 天)" if left is not None else ""), + f"有效期:{rec.not_before or '立即生效'} → {rec.expires_at or '永久'}", + f"签发时间:{rec.issued_at or '—'}", + f"密钥标识:{rec.kid or '—'}", + f"公钥指纹:{rec.public_key_fp or '—'}", + f"入库时间:{rec.created_at}(更新于 {rec.updated_at})", + f"入库来源:{rec.source or '—'}", + f"备注:{rec.note or '—'}", + "", + f"—— 各硬件来源指纹({len(rec.sources)} 个,续期即复用这些)——", + *[f" {k} = {v}" for k, v in sorted(rec.sources.items())], + "" if rec.sources else " (无:当初仅按机器码精确匹配签发)", + ])) + lay.addWidget(summary) + + raw = QPlainTextEdit() + raw.setReadOnly(True) + raw.setFont(make_mono_label().font()) + raw.setPlainText(json.dumps(doc, ensure_ascii=False, indent=2)) + lay.addWidget(QLabel("原始 license 文件内容:")) + lay.addWidget(raw) + + btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + btns.rejected.connect(dlg.reject) + btns.accepted.connect(dlg.accept) + lay.addWidget(btns) + dlg.exec() + + def _delete(self) -> None: + selected = self._selected_records() + if not selected: + warn(self, "未选中", "请选中要删除的记录。") + return + ids = [r.lic_id for r in selected] + from PySide6.QtWidgets import QMessageBox + + res = QMessageBox.question( + self, "确认删除", + "从台账中删除以下记录?\n\n" + "\n".join(ids) + + "\n\n注意:只删台账记录,不影响已经发给客户的 license 文件" + "(那些文件仍有效、仍可正常导入)。", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if res != QMessageBox.StandardButton.Yes: + return + reg = self.holder.ensure_open() + for lic_id in ids: + reg.delete(lic_id) + self.refresh() + + # ------------------------------------------------------------------ + # 导出 + # ------------------------------------------------------------------ + def _pick_outdir(self) -> None: + path = QFileDialog.getExistingDirectory( + self, "选择导出目录", self.out_edit.text() or str(Path.cwd()) + ) + if path: + self.out_edit.setText(path) + + def _require_records(self) -> bool: + if not self.records: + warn(self, "没有可导出的记录", "当前筛选结果为空,请先放宽筛选条件。") + return False + return True + + def _export_licenses(self) -> None: + if not self._require_records(): + return + out = self.out_edit.text().strip() + if not out: + warn(self, "未填目录", "请先选择导出目录。") + return + try: + files = self.holder.ensure_open().export_licenses( + out, self.records, subdir_by_customer=bool(self.by_customer.currentData()) + ) + except Exception as exc: # noqa: BLE001 + error(self, "导出失败", f"{exc}") + return + self.settings.setValue("registry/outdir", out) + info(self, "已导出", f"{len(files)} 个 .lic 已导出到\n{out}") + + def _export_csv(self) -> None: + if not self._require_records(): + return + default = str(Path(self.out_edit.text() or Path.cwd()) / "授权台账.csv") + path, _ = QFileDialog.getSaveFileName( + self, "保存 CSV 清单", default, "CSV 文件 (*.csv);;所有文件 (*)" + ) + if not path: + return + try: + csv_path = self.holder.ensure_open().export_csv(path, self.records) + except Exception as exc: # noqa: BLE001 + error(self, "导出失败", f"{exc}") + return + info(self, "已导出", + f"{csv_path}\n\n{len(self.records)} 条记录。UTF-8 BOM 编码,Excel 双击即可打开。") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fe66e61 --- /dev/null +++ b/uv.lock @@ -0,0 +1,408 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "chongming-license" +version = "0.1.0" +source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "orjson" }, + { name = "pydantic" }, +] +sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-license/0.1.0/chongming_license-0.1.0.tar.gz", hash = "sha256:30f3752a53ea76465c47ad324cbe1e4cfc656df175a9864f6d217d7b983d21c9" } +wheels = [ + { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-license/0.1.0/chongming_license-0.1.0-py3-none-any.whl", hash = "sha256:6369da005874c83a279d35cbaffcd606c494b49514e4896b665e6f19ba2aa36a" }, +] + +[[package]] +name = "chongming-license-gui" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "chongming-license" }, + { name = "pyside6" }, +] + +[package.metadata] +requires-dist = [ + { name = "chongming-license" }, + { name = "pyside6" }, +] + +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, +] + +[[package]] +name = "orjson" +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, +] + +[[package]] +name = "pyside6" +version = "6.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/b5/99ff75f604d69eda1c5d1fe85cc43fb697d95eabc30ba86af099c2ddcd96/pyside6-6.11.2-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:13a3c79816879d9743672a669f1988f74459a78f89a97a2624fae4fe059ffa3a", size = 571875, upload-time = "2026-08-18T06:37:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fe/968844def58150a41568b505edca0d68df6901b20ea4222ccb0c069cc703/pyside6-6.11.2-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:dc6d03990489a5085842770718392ef5de36352d8f608d9a9fe03e60af4d7b66", size = 572120, upload-time = "2026-08-18T06:37:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/f4ecc6defaec645e5ac3ca2f99e3da286a28d1c647c2d10b584e0392b94c/pyside6-6.11.2-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:57fe867a6c93821a085d74e8a26f863fc2363516848181c466032595b33b1729", size = 572121, upload-time = "2026-08-18T06:37:32.383Z" }, + { url = "https://files.pythonhosted.org/packages/94/fe/4aea9d30300fff4cda9bb729c1363b9e1095a8544e0d6b57065465e16599/pyside6-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:3201d67e3c10be2eaedd3910ff0f02351eca7e88c95a291cde5e7f2f55ef207f", size = 578382, upload-time = "2026-08-18T06:37:34.385Z" }, + { url = "https://files.pythonhosted.org/packages/26/b0/8e2fb01e9ee15ad29fe10d0dd2d37f503e4aa3714c9fbb07aca380618de7/pyside6-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:0444ac71d0791a19bded35f6d9a94515941f23a6eb7098ca45a1b64a338be697", size = 561768, upload-time = "2026-08-18T06:37:36.437Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/91/339fe9f7c94163815004ccd6b5c58b926b2dfb4e51c3b5563d412eab151f/pyside6_addons-6.11.2-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:354c574a839f7d0751960ba03f33bec87c95e0f2796dca3287f54d03abe97dac", size = 332047075, upload-time = "2026-08-18T06:35:33.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/cd/ce470e20f68901f4dfa5f742d6a3a58ba865684a232e4b87d5a1a2179cc0/pyside6_addons-6.11.2-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a8b00956925fbdeffc7052cf933506391289f35ac4f3f71a0a167ec195cd47b4", size = 175062576, upload-time = "2026-08-18T06:35:45.583Z" }, + { url = "https://files.pythonhosted.org/packages/11/bd/f725a8984703612c72cd17d0871d047132bacaccc13fc0d288a5e8a37dad/pyside6_addons-6.11.2-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a2ca3c73e5f060d4aca49451abd9fba68dd0e1c66ff40cea3a1fe0415def579a", size = 170827254, upload-time = "2026-08-18T06:35:56.675Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/08bf6594101b42fc88834009f1dac59a983c5b6805c62c5081f077d09b4a/pyside6_addons-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:f449ea4431da20e7b86752cca8d166f93434516fe417f981c27e5f8e1b554407", size = 168208836, upload-time = "2026-08-18T06:36:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/e4b7f09343e9f29e565f488fb44c4f1fe260db481ba2c08ad8a1a7d48216/pyside6_addons-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:d5606af369484b862b4d5741cfc179e19d2b8819c3e04d210b8188e0c9f85988", size = 35807245, upload-time = "2026-08-18T06:36:12.485Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/16/0b7ecf89ebada82ed0430be33809ff325761ece83104f8635b1bd101fcd0/pyside6_essentials-6.11.2-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:77795c145202e65a78d88f7cd409d186e3ba23d159bdb3ba2dcd159ae5e5f0d9", size = 110800753, upload-time = "2026-08-18T06:36:33.674Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1a/84a84bbc75835ee88abdcb66eb875b8e1a21834af9b833bc1893f2c1bfaf/pyside6_essentials-6.11.2-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:aaf9f25f0f324874085fa5b26a610318db8a8e243cf85bb3e5400595191c7778", size = 80108947, upload-time = "2026-08-18T06:36:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/60/4c/d92b08a2151d095b9a3725fa0e71b215ec686d40bbd5d8b702d21a9b5eaa/pyside6_essentials-6.11.2-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:d3ec6e1885c46e57f16364f38ea8040ffc5dc0b18341058f608fede3ba930567", size = 79575497, upload-time = "2026-08-18T06:36:46.97Z" }, + { url = "https://files.pythonhosted.org/packages/dc/09/d2662684aee4d3f027ff9b1e965f22301bc28ffc5b7f167658508e4475dc/pyside6_essentials-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:c8a29def77032773a30879f7f24415b5395ad08592d147c170824ef4c735dfc1", size = 76913043, upload-time = "2026-08-18T06:36:54.038Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/4dd2a6468cea600da1fcf9c7f02bc248eeaa98cdc75658e48e669d95326a/pyside6_essentials-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:fadd75c5c20800d64dd0a586ea8cb337c5e630aa2246df0e5105738afa46e02c", size = 58125520, upload-time = "2026-08-18T06:36:59.073Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.11.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/44/11bf71c36e71936ab4e923e5dd23599dab527c4488b01860b0f689ce9947/shiboken6-6.11.2-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:53659683b1f7a08e9f87eff9b1065f1ceb7110cd7a4bc09fdf5efe43d286604d", size = 476674, upload-time = "2026-08-18T06:37:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9a/f41bea4316d0c1bec4800842dcb4bd8fd3e29d6a79c496bc32e1aec42583/shiboken6-6.11.2-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7a7a0a72a9ed26c9bf77d42246b1c736486befb8f31aa2fb29957ea4cdd1c1c2", size = 272435, upload-time = "2026-08-18T06:37:13.397Z" }, + { url = "https://files.pythonhosted.org/packages/56/05/79ed0697515d4fc7435459e4ed4c4a3b5621a55b7c91d0f3f2cc3d617aa0/shiboken6-6.11.2-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:4bbbd6fa4d7cff5ec5e12bc4c10e1d845fa30c89457ecb13cc64f7bddb77f6f9", size = 270668, upload-time = "2026-08-18T06:37:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6b/fee1b4814317e4664e318644c9c007676c65a71e5b2b9d3ac4121a94dc0a/shiboken6-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:6ab0eba1c904455df621f9a6df3ca2bb896bab8670572d2bc4e37804ae91f19a", size = 1226578, upload-time = "2026-08-18T06:37:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b5/80c4e5aafeb8ac777cf5a1becfe87ac7197c35051df00b5f7819787e09d5/shiboken6-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:97c49432488df958f0308736f3d15b6915d8826fc380af17bb4b9ec5c4a5e81b", size = 1784669, upload-time = "2026-08-18T06:37:21.046Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +]