24 Commits

Author SHA1 Message Date
f52d7fc352 ci(publish): use repository-level PACKAGE_TOKEN secret
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
2026-05-06 18:21:35 +08:00
cff1408d87 ci(publish): use repository-level PACKAGE_TOKEN secret 2026-05-06 18:15:22 +08:00
f55c7bdc52 ci(publish): use auto-injected github.token with explicit permissions 2026-05-06 18:07:10 +08:00
ebdd8b562d ci(publish): align with internal runner conventions (python3.12 + intra-Gitea checkout + cn mirrors)
Some checks failed
Publish Python Package to Gitea / publish (push) Failing after 31s
2026-05-06 17:28:13 +08:00
d2dbb5f61e 重构项目路径
Some checks failed
Publish Python Package to Gitea / publish (push) Failing after 59s
2026-05-06 17:16:09 +08:00
8bbf5084f5 修改打包将test打包进去 2026-05-06 11:47:17 +08:00
b9cd4dacec 优化代码 2026-04-23 11:04:44 +08:00
d6ad90a7db 提交修改 2026-04-22 15:51:37 +08:00
2174b2b573 加入匠厂桌面测试 2026-04-20 13:40:53 +08:00
71a9ab1700 提交修改 2026-04-19 15:35:47 +08:00
ca117cb5ac 提交修改 2026-04-19 15:17:57 +08:00
892cf837a6 调整流水线 2026-04-16 15:06:49 +08:00
a6bbd89350 调整流水线 2026-04-16 13:58:32 +08:00
8778d641ab 调整流水线 2026-04-16 13:46:19 +08:00
5037d83b3d 调整流水线 2026-04-16 13:41:36 +08:00
8154de452b 调整流水线 2026-04-16 13:29:45 +08:00
0bb6707e68 调整流水线 2026-04-16 11:49:06 +08:00
4b15c6d99c ci: sync readme_md and description from references/README.md with SKILL.md fallback
Made-with: Cursor
2026-04-12 09:10:58 +08:00
4856167682 ci: deploy 用 cp 替代 rsync;同步 contrib 模板;更新产品说明 2026-04-10 13:39:54 +08:00
c67487ba16 ci: add reusable-release-frontend for static sites (fangzhen) 2026-04-10 13:31:08 +08:00
69702f8ea2 ci: add reusable-release-frontend for static sites (fangzhen) 2026-04-10 12:04:51 +08:00
60b4f7a77f release: python-runtime README, runtime_env sdk 2026-04-08 14:01:23 +08:00
7ac6e39ff9 release: python-runtime, runtime_env sync, docs 2026-04-08 12:06:14 +08:00
9926eb04cb chore: get_sibling_skills_root prefer co-located inference over env
Made-with: Cursor
2026-04-07 10:37:36 +08:00
26 changed files with 1980 additions and 53 deletions

39
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Publish Python Package to Gitea
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PATH: /usr/local/bin:/usr/local/python3.12/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple
PIP_EXTRA_INDEX_URL: https://mirrors.aliyun.com/pypi/simple https://mirrors.cloud.tencent.com/pypi/simple https://mirrors.huaweicloud.com/repository/pypi/simple
PIP_DEFAULT_TIMEOUT: "180"
PIP_TRUSTED_HOST: pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com mirrors.cloud.tencent.com mirrors.huaweicloud.com files.pythonhosted.org pypi.org
steps:
- uses: https://git.jc2009.com/admin/actions-checkout@v4
- name: Install build tools
run: python3.12 -m pip install --upgrade build twine --break-system-packages
- name: Build distributions
run: python3.12 -m build
- name: Publish to Gitea Package Registry
env:
TWINE_USERNAME: admin
TWINE_PASSWORD: ${{ secrets.PACKAGE_TOKEN }}
run: |
python3.12 -m twine upload \
--repository-url https://git.jc2009.com/api/packages/client-jiangchang/pypi \
dist/*

View File

@@ -0,0 +1,115 @@
# Standalone reusable workflow for Node/Vite static sites.
# Does not reference reusable-release-skill.yaml; skill workflows remain unchanged.
#
# Node请在 Runner 上预装 Node 20+(勿用 actions/setup-node避免每次从 GitHub 拉包)。
# npm通过 npm_registry 输入使用国内镜像(默认 npmmirror
# Checkout不用 actions/checkouthost 模式下 JS Action 仍会进无 Node 的容器);改用 git + github.token。
# 重要act_runner 会把 run: 脚本写到 $GITHUB_WORKSPACE/workflow/*.sh
# 所以 checkout 绝对不能在 workspace 根上 rm -rf ./*,否则会把自己这个脚本一起删了。
# 解决办法:克隆到 $GITHUB_WORKSPACE/_src 子目录,后续 step 都在该子目录操作。
# 浅拉不要用「裸 SHA」作 fetch 参数Gitea 上易失败);按分支 clone 再对齐 SHA。
name: Reusable Frontend Deploy
on:
workflow_call:
inputs:
deploy_path:
description: "Target directory for static files (trailing slash optional)"
required: false
type: string
default: "/www/wwwroot/sandbox/web"
build_command:
required: false
type: string
default: "npm run build:jc2009"
npm_registry:
description: "npm registry (e.g. npmmirror for CN)"
required: false
type: string
default: "https://registry.npmmirror.com"
runs_on:
description: "Runner label; must match a registered runner"
required: false
type: string
default: "ubuntu-latest"
chown_www:
description: "Run chown -R www:www after sync (requires permission on runner)"
required: false
type: boolean
default: true
jobs:
build-and-deploy:
runs-on: ${{ inputs.runs_on }}
defaults:
run:
shell: bash
working-directory: ${{ github.workspace }}/_src
env:
NPM_CONFIG_REGISTRY: ${{ inputs.npm_registry }}
steps:
- name: Checkout
# 这一步在 workspace 根执行,不能用默认的 _src此时还不存在
working-directory: ${{ github.workspace }}
env:
GITEA_HOST: git.jc2009.com
GITEA_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git config --global --add safe.directory '*'
REPO="${{ github.repository }}"
SHA="${{ github.sha }}"
BRANCH="${{ github.ref_name }}"
TOKEN="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}"
test -n "$TOKEN" || { echo "Checkout: empty token (github.token not available to this job)"; exit 1; }
URL="https://x-access-token:${TOKEN}@${GITEA_HOST}/${REPO}.git"
SRC_DIR="${GITHUB_WORKSPACE}/_src"
# 只清理 _src不动 workspace 根(保护 runner 写入的 workflow/*.sh
rm -rf "$SRC_DIR"
git clone --depth 1 --branch "$BRANCH" "$URL" "$SRC_DIR"
cd "$SRC_DIR"
if [ "$(git rev-parse HEAD)" != "$SHA" ]; then
git fetch --depth 200 origin "refs/heads/${BRANCH}"
git checkout --force "$SHA"
fi
echo "Checked out $(git rev-parse HEAD) to $SRC_DIR"
- name: Check Node.js (pre-installed on runner)
run: |
set -e
command -v node >/dev/null || { echo "Runner 上未找到 node请先安装 Node 20+"; exit 1; }
command -v npm >/dev/null || { echo "Runner 上未找到 npm"; exit 1; }
node -v
npm -v
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
test "$NODE_MAJOR" -ge 20 || { echo "需要 Node 20 及以上,当前: $(node -v)"; exit 1; }
- name: Install dependencies
run: npm ci
- name: Build
run: ${{ inputs.build_command }}
- name: Deploy (sync dist to web root)
run: |
set -euo pipefail
test -d dist
DEST="${{ inputs.deploy_path }}"
DEST="${DEST%/}"
mkdir -p "$DEST"
# 宝塔面板会自动生成 .user.ini(并加 immutable 属性)和 .htaccess,跳过它们
find "$DEST" -mindepth 1 -maxdepth 1 \
! -name ".user.ini" \
! -name ".htaccess" \
-exec rm -rf {} +
cp -a dist/. "$DEST/"
- name: Set ownership for Nginx (Baota www)
run: |
if [ "${{ inputs.chown_www }}" != "true" ]; then exit 0; fi
DEST="${{ inputs.deploy_path }}"
DEST="${DEST%/}"
chown -R www:www "$DEST" || chown -R nginx:nginx "$DEST" || true

View File

@@ -3,6 +3,11 @@ name: Reusable Skill Release
on:
workflow_call:
inputs:
runs_on:
description: "Runner label; must match a registered runner (use host runner for pip/python on same machine as Node frontend)"
required: false
type: string
default: ubuntu-latest
artifact_platform:
required: false
type: string
@@ -30,23 +35,30 @@ on:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
runs-on: ${{ inputs.runs_on }}
defaults:
run:
shell: bash
env:
ARTIFACT_PLATFORM: ${{ inputs.artifact_platform }}
PYARMOR_PLATFORM: ${{ inputs.pyarmor_platform }}
PIP_BREAK_SYSTEM_PACKAGES: "1"
# Prefer self-built Python 3.12 under /usr/local (Alibaba Cloud Linux host); keep system paths as fallback.
# 显式前缀,避免部分 Runner 未注入 env.PATH 时丢失系统路径
PATH: /usr/local/bin:/usr/local/python3.12/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
# PyArmor 交叉平台加密时会内部执行 pip 安装 pyarmor.cli.core.* 等包;不设则默认走 files.pythonhosted.org国内 CI 易超时。
PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple
PIP_EXTRA_INDEX_URL: https://mirrors.aliyun.com/pypi/simple https://mirrors.cloud.tencent.com/pypi/simple https://mirrors.huaweicloud.com/repository/pypi/simple
PIP_DEFAULT_TIMEOUT: "180"
PIP_TRUSTED_HOST: pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com mirrors.cloud.tencent.com mirrors.huaweicloud.com files.pythonhosted.org pypi.org
steps:
- uses: http://120.25.191.12:3000/admin/actions-checkout@v4
- uses: https://git.jc2009.com/admin/actions-checkout@v4
# Pin PyArmor 8.5.3 — matches desktop bundles; 9.x trial is stricter in CI。
# 镜像由 job envPIP_INDEX_URL / PIP_EXTRA_INDEX_URL统一指定与 Encrypt 步骤中 PyArmor 内部 pip 一致。
# 使用 python3.12 -m pip避免仅存在 python3(3.6) 或裸 pip 不在 PATH 的宿主机/容器。
- name: Setup Tools
run: pip install "pyarmor==8.5.3" requests python-frontmatter --break-system-packages
run: python3.12 -m pip install "pyarmor==8.5.3" requests python-frontmatter --break-system-packages
- name: Register PyArmor (optional)
env:
@@ -55,28 +67,86 @@ jobs:
if [ -z "${PYARMOR_REG_B64}" ]; then
echo "PyArmor: no PYARMOR_REG_B64 secret — trial mode (very large single .py modules may fail to obfuscate)."
else
python -c "import os,base64,pathlib,subprocess; p=pathlib.Path('/tmp/pyarmor-reg.zip'); p.write_bytes(base64.standard_b64decode(os.environ['PYARMOR_REG_B64'])); subprocess.run(['pyarmor','reg',str(p)],check=True); p.unlink(missing_ok=True)"
python3.12 -c "import os,base64,pathlib,subprocess; os.environ['PATH']='/usr/local/bin:/usr/local/python3.12/bin:'+os.environ.get('PATH',''); p=pathlib.Path('/tmp/pyarmor-reg.zip'); p.write_bytes(base64.standard_b64decode(os.environ['PYARMOR_REG_B64'])); subprocess.run(['pyarmor','reg',str(p)],check=True); p.unlink(missing_ok=True)"
fi
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
# Obfuscate scripts recursively. Copy package support files as plain text.
- name: Encrypt Source Code
run: |
export PATH="/usr/local/bin:/usr/local/python3.12/bin:${PATH:-}"
mkdir -p dist/package/scripts
set -euo pipefail
test -d scripts
( cd scripts && pyarmor gen --platform "${PYARMOR_PLATFORM}" -r -O ../dist/package/scripts . )
cp SKILL.md dist/package/
if [ -d references ]; then cp -r references dist/package/; fi
if [ -d assets ]; then cp -r assets dist/package/; fi
printf '%s\n' \
'import os' \
'import shutil' \
'' \
"PACKAGE = 'dist/package'" \
'' \
'def common_ignore(dir_path, names):' \
' ignored = set()' \
' for name in names:' \
' lower = name.lower()' \
" if name in ('__pycache__', '.pytest_cache', 'artifacts'):" \
' ignored.add(name)' \
" elif lower.endswith('.pyc') or lower.endswith('.pyo') or lower.endswith('.db'):" \
' ignored.add(name)' \
' return list(ignored)' \
'' \
"ref_src = os.path.abspath('references')" \
'' \
'def references_ignore(dir_path, names):' \
' ignored = set(common_ignore(dir_path, names))' \
" if os.path.abspath(dir_path) == ref_src and 'REQUIREMENTS.md' in names:" \
" ignored.add('REQUIREMENTS.md')" \
' return list(ignored)' \
'' \
'def copy_dir(src, dst, ignore_fn):' \
' if os.path.isdir(src):' \
' shutil.copytree(src, dst, ignore=ignore_fn, dirs_exist_ok=True)' \
'' \
"shutil.copy2('SKILL.md', os.path.join(PACKAGE, 'SKILL.md'))" \
"copy_dir('references', os.path.join(PACKAGE, 'references'), references_ignore)" \
"copy_dir('assets', os.path.join(PACKAGE, 'assets'), common_ignore)" \
"copy_dir('tests', os.path.join(PACKAGE, 'tests'), common_ignore)" \
"copy_dir('evals', os.path.join(PACKAGE, 'evals'), common_ignore)" \
'' \
"print('Package top-level entries:')" \
'for name in sorted(os.listdir(PACKAGE)):' \
" print(' -', name)" \
> /tmp/package_skill_release_copy.py
python3.12 /tmp/package_skill_release_copy.py
- name: Parse Metadata and Pack
id: build_task
run: |
python -c "
python3.12 -c "
import frontmatter, os, json, shutil
post = frontmatter.load('SKILL.md')
metadata = dict(post.metadata or {})
metadata['readme_md'] = (post.content or '').strip()
skill_readme_md = (post.content or '').strip()
skill_description = metadata.get('description')
metadata['readme_md'] = skill_readme_md
readme_path = os.path.join('references', 'README.md')
if os.path.isfile(readme_path):
try:
readme_post = frontmatter.load(readme_path)
body = (readme_post.content or '').strip()
rm_meta = readme_post.metadata or {}
desc = rm_meta.get('description')
if desc is not None and str(desc).strip():
metadata['description'] = str(desc).strip()
if body:
metadata['readme_md'] = body
except Exception:
pass
if not (metadata.get('readme_md') or '').strip():
metadata['readme_md'] = skill_readme_md
if skill_description is not None and not str(metadata.get('description', '') or '').strip():
metadata['description'] = skill_description
openclaw_meta = metadata.get('metadata', {}).get('openclaw', {})
slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip()
if not slug:
@@ -102,7 +172,7 @@ jobs:
METADATA_JSON: ${{ steps.build_task.outputs.metadata }}
SYNC_URL: ${{ inputs.sync_url }}
run: |
python -c "
python3.12 -c "
import requests, json, os
metadata = json.loads(os.environ['METADATA_JSON'])
res = requests.post(os.environ['SYNC_URL'], json=metadata)
@@ -121,7 +191,7 @@ jobs:
ARTIFACT_PLATFORM: ${{ steps.build_task.outputs.artifact_platform }}
UPLOAD_URL: ${{ inputs.upload_url }}
run: |
python -c "
python3.12 -c "
import requests, os
slug = os.environ['SLUG']
version = os.environ['VERSION']
@@ -148,7 +218,7 @@ jobs:
VERSION: ${{ steps.build_task.outputs.version }}
PRUNE_URL: ${{ inputs.prune_url }}
run: |
python -c "
python3.12 -c "
import requests, os
payload = {
'name': os.environ['SLUG'],

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
__pycache__/
*.py[cod]
**/*.egg-info/
.env
.python-version
build/
dist/
python-runtime/.venv/

View File

@@ -1,7 +1,65 @@
# jiangchang-platform-kit
Shared platform components for Jiangchang skills:
## 1. 项目简介
- `sdk/jiangchang_skill_core`: entitlement SDK package.
- `.github/workflows/reusable-release-skill.yaml`: reusable CI release workflow.
- `examples/workflows/use-reusable-release-skill.yaml`: caller workflow sample.
匠厂平台共享 Python SDK在同一发行包中提供 **Skill 侧通用能力**(权益校验、统一日志与运行环境解析)与 **桌面应用自动化能力**(通过 CDP 连接匠厂客户端,驱动聊天与断言)。
## 2. 安装方式
```bash
pip install jiangchang-platform-kit --index-url https://git.jc2009.com/api/packages/client-jiangchang/pypi/simple/
```
## 3. 包含的模块
- **jiangchang_skill_core**:权益 HTTP 客户端、`enforce_entitlement`、统一文件日志、`JIANGCHANG_*` 数据根与技能根路径解析等,供 Skill CLI / 服务进程使用。
- **jiangchang_desktop_sdk**:匠厂桌面应用自动化客户端(基于 CDP + Playwright 同步 API。**本包不声明 Playwright 依赖**;请在运行环境中自行安装 Playwright以便 `import playwright` 可用。
## 4. 快速开始
### jiangchang_skill_core
```python
import os
from jiangchang_skill_core import (
EntitlementClient,
enforce_entitlement,
setup_skill_logging,
get_skill_logger,
)
os.environ.setdefault("JIANGCHANG_AUTH_BASE_URL", "https://auth.example.com")
os.environ.setdefault("JIANGCHANG_AUTH_API_KEY", "secret")
setup_skill_logging(skill_slug="demo-skill", logger_name="demo")
log = get_skill_logger()
log.info("skill started")
# client = EntitlementClient()
# enforce_entitlement(user_id="u1", skill_slug="demo-skill")
```
### jiangchang_desktop_sdk
```python
from jiangchang_desktop_sdk import JiangchangDesktopClient, AskOptions
# 需已安装 playwright且匠厂客户端可按 CDP 端口暴露调试接口
client = JiangchangDesktopClient()
client.ensure_app_running(wait_timeout_s=30.0)
try:
answer = client.ask("你好", options=AskOptions(new_task=True))
print(answer)
finally:
client.disconnect()
```
## 5. 发版流程(维护者)
```bash
git tag v0.1.0
git push origin v0.1.0
# CI 自动打包并发布到 Gitea Package Registry见 .github/workflows/publish.yml
```
仓库内还保留 `examples/``tools/``python-runtime/` 目录及既有 Skill / 前端相关的 GitHub Actions 工作流,与本 PyPI 包的发布彼此独立。

View File

@@ -5,8 +5,7 @@ on:
jobs:
release:
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@v0.1.0
uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@v0.1.0
with:
artifact_platform: windows
pyarmor_platform: windows.x86_64
include_readme_md: false

21
pyproject.toml Normal file
View File

@@ -0,0 +1,21 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "jiangchang-platform-kit"
version = "0.1.0"
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{ name = "client-jiangchang" }]
dependencies = [
"requests>=2.31.0",
]
[project.urls]
Repository = "https://git.jc2009.com/client-jiangchang/jiangchang-platform-kit"
[tool.setuptools.packages.find]
where = ["src"]

12
python-runtime/README.md Normal file
View File

@@ -0,0 +1,12 @@
# 匠厂共享 Python 运行时目录JIANGCHANG
本目录保留为技能运行时安装的**目标路径约定**:宿主可将第三方依赖集合同步到用户机器的
`{JIANGCHANG_DATA_ROOT}/python-runtime/`
并在该目录维护虚拟环境(例如使用 `uv sync``pip install -r requirements.txt`)。
**说明**:本仓库已改为在根目录通过 `pyproject.toml` 发布 **`jiangchang-platform-kit`** 单一 Python 包;原先在此处单独声明的 `pyproject.toml` / `uv.lock` 已移除。
Playwright、OpenAI SDK 等与具体技能相关的依赖,请由各技能或宿主 **`resources/jiangchang-python-runtime/`**(桌面应用内嵌模板)继续维护;与本仓库中的 SDK 包版本号无关。
**Playwright**:仅安装 Python 包 `playwright` 即可;技能可通过 **`channel=chrome` / `msedge`** 使用用户本机已安装的 Chrome 或 Edge。宿主流程一般不必执行 `playwright install chromium`(除非你希望使用自带 Chromium

View File

@@ -1,19 +0,0 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "jiangchang-skill-core"
version = "0.1.0"
description = "Common entitlement SDK for Jiangchang skills"
requires-python = ">=3.10"
dependencies = [
"requests>=2.31.0",
]
[tool.setuptools]
package-dir = {"" = "."}
[tool.setuptools.packages.find]
where = ["."]
include = ["jiangchang_skill_core*"]

View File

@@ -0,0 +1,38 @@
from .exceptions import (
AppNotFoundError,
AssertError,
ConnectionError,
GatewayDownError,
JiangchangDesktopError,
LaunchError,
TimeoutError,
)
from .types import AskOptions, AssertOptions, JiangchangMessage, LaunchOptions
__all__ = [
"JiangchangDesktopClient",
"JiangchangMessage",
"AskOptions",
"LaunchOptions",
"AssertOptions",
"JiangchangDesktopError",
"AppNotFoundError",
"ConnectionError",
"TimeoutError",
"AssertError",
"LaunchError",
"GatewayDownError",
]
__version__ = "0.1.0"
def __getattr__(name: str):
if name == "JiangchangDesktopClient":
from .client import JiangchangDesktopClient
return JiangchangDesktopClient
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__() -> list[str]:
return sorted({*__all__, "__version__"})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
class JiangchangDesktopError(Exception):
"""基类"""
pass
class AppNotFoundError(JiangchangDesktopError):
"""Electron 可执行文件找不到时抛出"""
pass
class ConnectionError(JiangchangDesktopError, ConnectionError):
"""连接桌面应用失败时抛出"""
pass
class TimeoutError(JiangchangDesktopError, TimeoutError):
"""操作超时"""
pass
class AssertError(JiangchangDesktopError):
"""断言失败时抛出,消息要包含期望值和实际值"""
pass
class LaunchError(JiangchangDesktopError):
"""应用启动失败时抛出"""
pass
class GatewayDownError(JiangchangDesktopError):
"""Gateway 在等待过程中被检测到已停止/退出时抛出,便于测试立刻失败而不是空等超时。"""
pass

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""匠厂桌面 E2E 与集成测试常用工具子包。
提供:
- ``SkillInfo`` / ``discover_skill_root`` / ``parse_skill_md``
- ``skill_healthcheck``
- ``HostAPIClient`` / ``HostAPIError``
**不提供任何会话清理能力**:测试完全模拟真实用户的 UI 操作,
真实用户从不会去后台删会话,测试也不该走底层 API / 文件系统去清。
"""
from .config import SkillInfo, discover_skill_root, parse_skill_md
from .healthcheck import HealthCheckError, skill_healthcheck
from .host_api import HostAPIClient, HostAPIError
__all__ = [
"SkillInfo",
"discover_skill_root",
"parse_skill_md",
"HealthCheckError",
"skill_healthcheck",
"HostAPIClient",
"HostAPIError",
]

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""Skill 元信息读取。
负责:
- 定位 skill 根目录(含 SKILL.md
- 解析 SKILL.md 的 YAML frontmatter抽出 slug / version / name 等关键字段。
为避免硬性依赖 PyYAML这里用一个**足够用**的轻量正则 parser只抽取
我们真正关心的字段(顶层 `name` / `version` / `author`,嵌套 `openclaw.slug`
与 `openclaw.category`)。更复杂的 YAML 结构请自行用 PyYAML 解析。
"""
from __future__ import annotations
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
SKILL_MD = "SKILL.md"
SKILL_ROOT_ENV = "JIANGCHANG_E2E_SKILL_ROOT"
@dataclass
class SkillInfo:
root: str
slug: str
name: str
version: str
author: str = ""
category: str = ""
def discover_skill_root(start: Optional[str] = None) -> str:
"""定位 skill 根目录。
优先级:
1. 环境变量 ``JIANGCHANG_E2E_SKILL_ROOT``(由调用方或测试入口提前注入);
2. 从 ``start``(默认 cwd向上回溯直到找到含 ``SKILL.md`` 的目录。
"""
env = (os.environ.get(SKILL_ROOT_ENV) or "").strip()
if env:
env_abs = os.path.abspath(env)
if os.path.isfile(os.path.join(env_abs, SKILL_MD)):
return env_abs
cur = Path(start or os.getcwd()).resolve()
for parent in [cur, *cur.parents]:
if (parent / SKILL_MD).exists():
return str(parent)
raise FileNotFoundError(
f"未能在 {start or os.getcwd()} 及其父目录中找到 {SKILL_MD}"
f"请设置环境变量 {SKILL_ROOT_ENV}=<skill 根目录绝对路径>。"
)
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
def _extract_frontmatter(text: str) -> str:
match = _FRONTMATTER_RE.match(text)
if not match:
raise ValueError("SKILL.md 缺少 YAML frontmatter--- ... ---)。")
return match.group(1)
def _strip_quotes(value: str) -> str:
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
return v[1:-1]
return v
def parse_skill_md(skill_root: str) -> SkillInfo:
"""解析 SKILL.md 的 frontmatter返回 SkillInfo。"""
md_path = Path(skill_root) / SKILL_MD
if not md_path.exists():
raise FileNotFoundError(f"SKILL.md 不存在:{md_path}")
text = md_path.read_text(encoding="utf-8")
fm = _extract_frontmatter(text)
top: dict[str, str] = {}
openclaw: dict[str, str] = {}
lines = fm.splitlines()
i = 0
while i < len(lines):
line = lines[i]
if not line.strip() or line.lstrip().startswith("#"):
i += 1
continue
# 顶层 key: value
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
if m:
key, rest = m.group(1), m.group(2)
if rest.strip() == "":
# 嵌套块,收集 2 空格缩进的子项
i += 1
sub: dict[str, str] = {}
while i < len(lines):
sub_line = lines[i]
if sub_line.strip() == "" or sub_line.lstrip().startswith("#"):
i += 1
continue
if not sub_line.startswith(" "):
break
sm = re.match(r"^\s+([A-Za-z_][\w-]*):\s*(.*)$", sub_line)
if sm:
sub[sm.group(1)] = _strip_quotes(sm.group(2))
i += 1
if key == "metadata":
# openclaw 在 metadata 下再下一层:
# metadata:
# openclaw:
# slug: ...
# 上面 sub 已经拿到的只是 "openclaw" 这个 key。为稳妥起见回头
# 用一个更宽松的扫描:直接找所有含 `slug:` `category:` 的行。
pass
continue
top[key] = _strip_quotes(rest)
i += 1
# openclaw.slug / category 用宽松扫描兜底(无论嵌套几层)
for line in lines:
stripped = line.lstrip()
if stripped.startswith("slug:") and "slug" not in openclaw:
openclaw["slug"] = _strip_quotes(stripped.split(":", 1)[1])
elif stripped.startswith("category:") and "category" not in openclaw:
openclaw["category"] = _strip_quotes(stripped.split(":", 1)[1])
return SkillInfo(
root=os.path.abspath(skill_root),
slug=openclaw.get("slug", "") or Path(skill_root).name,
name=top.get("name", Path(skill_root).name),
version=top.get("version", "0.0.0"),
author=top.get("author", ""),
category=openclaw.get("category", ""),
)

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""技能健康检查。
检查项:
1. SKILL.md 成功解析,含 slug / version。
2. ``python scripts/main.py health`` 命令退出码为 0每个 skill 都约定实现)。
"""
from __future__ import annotations
import logging
import os
import subprocess
import sys
from .config import SkillInfo
logger = logging.getLogger("jiangchang_desktop_sdk.testing.healthcheck")
class HealthCheckError(RuntimeError):
"""健康检查失败。"""
def skill_healthcheck(
skill_info: SkillInfo,
*,
run_cli: bool = True,
timeout_s: float = 15.0,
) -> None:
"""对一个 skill 跑本地健康检查。失败抛 ``HealthCheckError``。"""
if not skill_info.slug:
raise HealthCheckError(f"SKILL.md 未定义 openclaw.slug{skill_info.root}")
if not skill_info.version or skill_info.version == "0.0.0":
raise HealthCheckError(f"SKILL.md 未定义 version{skill_info.root}")
if not run_cli:
return
main_py = os.path.join(skill_info.root, "scripts", "main.py")
if not os.path.isfile(main_py):
logger.warning("skill_healthcheck: %s 不存在,跳过 CLI health 检查", main_py)
return
cmd = [sys.executable, main_py, "health"]
logger.debug("skill_healthcheck: 运行 %s (cwd=%s)", cmd, skill_info.root)
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout_s,
cwd=skill_info.root,
encoding="utf-8",
errors="replace",
)
except subprocess.TimeoutExpired as exc:
raise HealthCheckError(
f"skill_healthcheck: '{' '.join(cmd)}' 超时 {timeout_s}s"
) from exc
if proc.returncode != 0:
raise HealthCheckError(
f"skill_healthcheck: '{' '.join(cmd)}' 退出码 {proc.returncode}\n"
f"stdout: {proc.stdout[:600]}\n"
f"stderr: {proc.stderr[:600]}"
)
logger.info(
"skill_healthcheck: %s v%s OK", skill_info.slug, skill_info.version
)

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""匠厂 Host HTTP API 的 Python 轻封装。
使用 stdlib urllib避免为 SDK 引入新运行时依赖。当前只暴露本模块
真正需要的端点健康探活、agent 列表),**不包含任何会话删除/会话
文件系统操作**——测试侧要保持"只通过 UI 操作,不走底层接口"的原则,
后续如需访问其他 host-api 端点请在此处按需添加读型方法。
Host API 默认端口:`13210`(见 jiangchang/electron/utils/config.ts 中 `CLAWX_HOST_API`)。
可通过环境变量 ``CLAWX_PORT_CLAWX_HOST_API`` 或本类的 ``port`` 参数覆盖。
"""
from __future__ import annotations
import json
import logging
import os
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
DEFAULT_PORT = 13210
DEFAULT_HOST = "127.0.0.1"
logger = logging.getLogger("jiangchang_desktop_sdk.testing.host_api")
class HostAPIError(RuntimeError):
"""Host API 调用失败(网络错误或非 2xx 响应)。"""
class HostAPIClient:
def __init__(
self,
host: str = DEFAULT_HOST,
port: Optional[int] = None,
timeout: float = 10.0,
) -> None:
self.host = host
env_port = os.environ.get("CLAWX_PORT_CLAWX_HOST_API")
self.port = port or (int(env_port) if env_port else DEFAULT_PORT)
self.timeout = timeout
@property
def base_url(self) -> str:
return f"http://{self.host}:{self.port}"
def _request(self, method: str, path: str, body: Any = None) -> Any:
url = f"{self.base_url}{path}"
data = None
headers = {"Accept": "application/json"}
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
raw = resp.read().decode("utf-8")
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw
except urllib.error.HTTPError as exc:
payload: Any
try:
payload = json.loads(exc.read().decode("utf-8"))
except Exception: # noqa: BLE001
payload = str(exc)
raise HostAPIError(f"{method} {path} -> HTTP {exc.code}: {payload}") from exc
except urllib.error.URLError as exc:
raise HostAPIError(f"{method} {path} -> {exc}") from exc
def ping(self) -> bool:
try:
self.list_agents()
return True
except HostAPIError:
return False
def list_agents(self) -> Dict[str, Any]:
return self._request("GET", "/api/agents") or {}

View File

@@ -0,0 +1,48 @@
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class JiangchangMessage:
"""单条会话消息"""
id: str
role: str # 'user' | 'assistant' | 'system' | 'tool'
content: str
timestamp: float
is_error: bool = False
tool_call_id: Optional[str] = None
tool_name: Optional[str] = None
tool_status: Optional[str] = None # 'running' | 'completed' | 'error'
thinking_content: Optional[str] = None
@dataclass
class AskOptions:
"""ask() 方法的选项"""
timeout: int = 120000 # 毫秒
wait_for_tools: bool = True
agent_id: str = "main"
# 拟人化输入每个字符间延迟毫秒。0 = 直接 fill()。
typing_delay_ms: int = 25
# 发送方式True=按 Enter 键False=点击发送按钮
use_enter_key: bool = True
# 每次 ask 前是否自动新建任务(避免上下文污染)
new_task: bool = True
# 流式输出判稳阈值(秒):助手消息文本连续该秒数无增长视为完成
stable_seconds: float = 3.0
# 轮询间隔(秒)
poll_interval: float = 0.5
@dataclass
class LaunchOptions:
"""launch_app() 方法的选项"""
executable_path: Optional[str] = None # 默认从 JIANGCHANG_E2E_APP_PATH 环境变量读取
cdp_port: int = 9222
startup_timeout: int = 30000
headless: bool = False # 是否无头模式运行
@dataclass
class AssertOptions:
"""assert_contains() 方法的选项"""
timeout: int = 5000
match_mode: str = "contains" # 'contains' | 'regex' | 'exact'
message_index: int = -1 # -1 表示最后一条 assistant 消息
include_tools: bool = False

View File

@@ -1,4 +1,5 @@
from .client import EntitlementClient
from .errors import EntitlementDeniedError, EntitlementError, EntitlementServiceError
from .guard import enforce_entitlement
from .models import EntitlementResult
from .runtime_env import (
@@ -21,7 +22,10 @@ from .unified_logging import (
__all__ = [
"EntitlementClient",
"EntitlementDeniedError",
"EntitlementError",
"EntitlementResult",
"EntitlementServiceError",
"apply_cli_local_defaults",
"attach_unified_file_handler",
"enforce_entitlement",

View File

@@ -1,7 +1,12 @@
"""
JIANGCHANG_* 数据根与用户目录解析规则 + 可选本地 CLI 默认值
各技能 scripts/jiangchang_skill_core/ 为发布用副本 kit 保持同步
宿主在 skills.entries Gateway 中注入
- PATH 前缀{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/(Scripts|bin)
- VIRTUAL_ENVJIANGCHANG_PYTHON_EXE
Playwright 使用本机 Chrome/Edgelaunch channel=chrome|msedge不依赖宿主下载的 Chromium
技能勿在仓库内维护独立 .venv共享逻辑请使用本仓库发布的 ``jiangchang-platform-kit``
其余运行时依赖 Playwright模型 SDK由宿主或技能各自的环境策略决定
"""
from __future__ import annotations
@@ -47,6 +52,8 @@ def _looks_like_skills_root(path: str) -> bool:
"content-manager",
"account-manager",
"sohu-publisher",
"toutiao-publisher",
"logistics-tracker",
"api-key-vault",
):
if os.path.isdir(os.path.join(path, marker)):
@@ -92,20 +99,22 @@ def get_skills_root() -> str:
def get_sibling_skills_root(skill_scripts_dir: str | None = None) -> str:
"""
编排子进程调用兄弟技能时用优先环境变量否则从本技能 scripts 目录推断并列根
OpenClaw 开发仓与网关 skills/<slug>/scripts 均满足技能根之上一级 = 并列根
编排子进程调用兄弟技能时用先根据本技能 scripts 目录推断并列根若目录下能识别出兄弟技能
则用之开发仓 D:\\OpenClaw 与安装目录 ~/.openclaw/skills 均满足技能根之上一级 = 并列根
避免全局 JIANGCHANG_SKILLS_ROOT / CLAW_SKILLS_ROOT 指向与当前检出不一致时错调兄弟进程
推断失败时再读上述环境变量最后回落 get_skills_root()
"""
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
v = (os.getenv(key) or "").strip()
if v:
return os.path.normpath(v)
if skill_scripts_dir:
scripts = os.path.abspath(skill_scripts_dir)
skill_root = os.path.dirname(scripts)
inferred = os.path.dirname(skill_root)
if _looks_like_skills_root(inferred):
return inferred
return os.path.normpath(inferred)
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
v = (os.getenv(key) or "").strip()
if v:
return os.path.normpath(v)
return get_skills_root()

View File

@@ -1,8 +1,6 @@
"""
统一文件日志{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log
按日轮转行内带 trace_id skill_slug便于跨技能排查
实现为各技能 scripts/jiangchang_skill_core/ 下的同名副本之源修改后请同步到各技能
"""
from __future__ import annotations

View File

@@ -25,8 +25,13 @@
.NOTES
Requires: git, PowerShell 5+
加密与 ZIP 内容由 CI 工作流 reusable-release-skill.yaml 的「Encrypt Source Code」步骤执行
scripts/ 递归 PyArmor-r,输出到包内 scripts/与源码目录树一致),并复制 SKILL.md、references/、assets/(若存在)。
加密与 ZIP 内容由 CI 工作流 reusable-release-skill.yaml 的「Encrypt Source Code」步骤执行。CI 会
- 加密 scripts/递归 PyArmor -r输出到包内 scripts/与源码目录树一致)
- 复制 SKILL.md
- 复制 references/(排除 REQUIREMENTS.md
- 复制 assets/
- 复制 tests/
- 复制 evals/(除 scripts 外均为明文;若目录不存在则跳过;复制时排除常见缓存与运行产物)
本脚本在打 tag 前会做一次 scripts/ 结构自检,避免子目录未提交却仍发布。
#>
@@ -46,7 +51,9 @@ $ErrorActionPreference = "Stop"
function Invoke-Git {
param([Parameter(Mandatory = $true)][string]$Args)
Write-Host ">> git $Args" -ForegroundColor DarkGray
& cmd /c "git $Args"
# 将 git 的 stderr 合并进 stdout避免 PowerShell 在 $ErrorActionPreference=Stop 下
# 把 "remote: ..." / "warning: LF -> CRLF" 等非错误输出误判为异常。
& cmd /c "git $Args 2>&1"
if ($LASTEXITCODE -ne 0) {
throw "git command failed: git $Args"
}
@@ -216,7 +223,9 @@ try {
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
}
$upstream = (& git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null)
# 用 cmd /c + 2>&1 将 stderr 合并进 stdout避免 PowerShell 在 $ErrorActionPreference=Stop
# 下把首次发布时 "fatal: ambiguous argument '@{u}'" 之类的 stderr 误判为异常。
$upstream = & cmd /c 'git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>&1'
$hasUpstream = ($LASTEXITCODE -eq 0)
if ($DryRun) {

161
uv.lock generated Normal file
View File

@@ -0,0 +1,161 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "certifi"
version = "2026.4.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" },
{ url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" },
{ url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" },
{ url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" },
{ url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" },
{ url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" },
{ url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" },
{ url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" },
{ url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" },
{ url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" },
{ url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" },
{ url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" },
{ url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" },
{ url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" },
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "idna"
version = "3.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
]
[[package]]
name = "jiangchang-platform-kit"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "requests" },
]
[package.metadata]
requires-dist = [{ name = "requests", specifier = ">=2.31.0" }]
[[package]]
name = "requests"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]