Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ firewall, with the same polish you'd expect from a public registry.

- 📖 **[User Guide](https://iflytek.github.io/skillhub/)** — Skill publishing, search, CLI usage and other user guides
- 🛠️ **[Developer Docs](https://zread.ai/iflytek/skillhub)** — Architecture, API reference, local development, deployment and operations
- 🐍 **[Python Examples](./examples/python)** — Search, download, and publish skills from Python via the REST API

## Highlights

Expand Down
1 change: 1 addition & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ SkillHub 是一个自托管平台,为团队提供私有的、受治理的智

- 📖 **[用户指南](https://iflytek.github.io/skillhub/)** — 技能发布、搜索、CLI 使用等用户操作指南
- 🛠️ **[开发者文档](https://zread.ai/iflytek/skillhub)** — 架构设计、API 参考、本地开发、部署运维等技术文档
- 🐍 **[Python 示例](./examples/python)** — 使用 REST API 在 Python 中搜索、下载和发布技能

## 核心特性

Expand Down
85 changes: 85 additions & 0 deletions examples/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# SkillHub Python Examples

A minimal, dependency-light (`requests`-only) Python client and runnable
examples for the SkillHub REST API. Use it to **search, inspect, download,
and publish** skills from Python — the same operations the ClawHub CLI
performs, without shelling out to the CLI.

> These are reference examples, not (yet) an officially published pip
> package. See [iflytek/skillhub#701](https://github.com/iflytek/skillhub/issues/701)
> for the discussion on whether to ship a full published SDK.

## Files

| File | What it is |
|------|------------|
| [`skillhub_client.py`](./skillhub_client.py) | A small `SkillHubClient` class wrapping the REST API |
| [`example_usage.py`](./example_usage.py) | Runnable script: search → resolve → download, and publish |
| [`requirements.txt`](./requirements.txt) | The only dependency: `requests` |

## Setup

```bash
pip install -r requirements.txt

# Point at your SkillHub instance
export SKILLHUB_URL=https://skill.example.com
# Only needed for write operations (publish / star / rate)
export SKILLHUB_TOKEN=<your-api-token>
```

Generate an API token from the SkillHub web UI (**Settings → API Tokens**) or
via `POST /api/v1/tokens`.

## Quick start

```python
from skillhub_client import SkillHubClient

client = SkillHubClient() # reads SKILLHUB_URL / SKILLHUB_TOKEN from env

# Search public skills
results = client.search(keyword="email", size=5)

# Inspect and resolve a version
detail = client.get_skill("my-namespace", "my-skill")
resolved = client.resolve("my-namespace", "my-skill", tag="stable")

# Download the latest package (returns the written file path)
path = client.download("my-namespace", "my-skill")

# Publish a skill package (requires a token)
client.publish("./my-skill.zip", namespace="my-namespace")
```

Or run the end-to-end script:

```bash
python example_usage.py # search + inspect + download
python example_usage.py publish ./my-skill.zip my-namespace
```

## Supported operations

| Method | Endpoint | Auth |
|--------|----------|------|
| `search(keyword, namespace, page, size)` | `GET /api/v1/skills` | — |
| `get_skill(namespace, slug)` | `GET /api/v1/skills/{ns}/{slug}` | — |
| `list_versions(namespace, slug)` | `GET /api/v1/skills/{ns}/{slug}/versions` | — |
| `resolve(namespace, slug, version, tag)` | `GET /api/v1/skills/{ns}/{slug}/resolve` | — |
| `download(namespace, slug, version, dest)` | `GET /api/v1/skills/{ns}/{slug}[/versions/{v}]/download` | — |
| `whoami()` | `GET /api/v1/whoami` | Bearer |
| `publish(zip_path, namespace, request_id)` | `POST /api/v1/publish` | Bearer |
| `star(namespace, slug)` | `POST /api/v1/skills/{ns}/{slug}/star` | Bearer |
| `rate(namespace, slug, score)` | `POST /api/v1/skills/{ns}/{slug}/rating` | Bearer |

The client unwraps the unified `{code, msg, data}` response envelope
automatically and raises `SkillHubError` on a non-zero business code.

## Notes

- Write operations accept an optional `request_id` (a UUID) that is sent as
the `X-Request-Id` header for idempotency.
- For the full API surface (namespaces, reviews, promotion, tags), see the
[Developer Docs → API](https://iflytek.github.io/skillhub/) and
[`document/docs/04-developer/api`](../../document/docs/04-developer/api).
90 changes: 90 additions & 0 deletions examples/python/example_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Runnable examples for the SkillHub Python client.

Configure the target registry via environment variables:

export SKILLHUB_URL=https://skill.example.com
export SKILLHUB_TOKEN=<your-api-token> # only needed for write operations

Then run:

python example_usage.py # search + inspect + download
python example_usage.py publish ./my-skill.zip my-namespace
"""

from __future__ import annotations

import os
import sys

from skillhub_client import SkillHubClient, SkillHubError


def _pick(obj, *keys, default=None):
"""Best-effort field access across slightly different response shapes."""
for key in keys:
if isinstance(obj, dict) and obj.get(key) is not None:
return obj[key]
return default


def demo_read(client: SkillHubClient) -> None:
print(f"Searching {client.base_url} for skills matching 'email'...\n")
result = client.search(keyword="email", size=5)

# The search payload may expose the list under 'items' or 'results'.
items = _pick(result, "items", "results", default=result if isinstance(result, list) else [])
if not items:
print("No skills found. Try a different keyword or registry.")
return

for skill in items:
name = _pick(skill, "name", "slug", default="(unnamed)")
ns = _pick(skill, "namespace", default="")
version = _pick(skill, "version", "latestVersion", default="?")
downloads = _pick(skill, "downloadCount", "downloads", default=0)
coord = f"{ns}/{name}" if ns else name
print(f" - {coord} v{version} ({downloads} downloads)")

# Download the first result's latest package.
first = items[0]
ns = _pick(first, "namespace", default="")
slug = _pick(first, "slug", "name")
if ns and slug:
print(f"\nResolving latest version of {ns}/{slug}...")
resolved = client.resolve(ns, slug)
version = _pick(resolved, "version", default=None)
print(f" resolved version: {version}")

dest = client.download(ns, slug, version=version)
size = os.path.getsize(dest)
print(f" downloaded -> {dest} ({size} bytes)")


def demo_publish(client: SkillHubClient, zip_path: str, namespace: str) -> None:
if not client.token:
sys.exit("Publishing requires SKILLHUB_TOKEN to be set.")
print(f"Publishing {zip_path} to namespace '{namespace}'...")
result = client.publish(zip_path, namespace)
print(f" published: {result}")


def main() -> None:
try:
client = SkillHubClient()
except ValueError as exc:
sys.exit(str(exc))

args = sys.argv[1:]
try:
if args and args[0] == "publish":
if len(args) != 3:
sys.exit("usage: python example_usage.py publish <zip_path> <namespace>")
demo_publish(client, args[1], args[2])
else:
demo_read(client)
except SkillHubError as exc:
sys.exit(f"API error: {exc}")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests>=2.25
Loading
Loading