Initial commit: add all skills
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
260
gitea-tea-skill/scripts/gitea_api.py
Normal file
260
gitea-tea-skill/scripts/gitea_api.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gitea_api.py — Pure stdlib Gitea API helper for https://git.shumengya.top
|
||||
|
||||
Usage:
|
||||
python scripts/gitea_api.py repos list
|
||||
python scripts/gitea_api.py repos create <name> [--desc DESC] [--private]
|
||||
python scripts/gitea_api.py repos delete <name>
|
||||
python scripts/gitea_api.py repos exists <name>
|
||||
python scripts/gitea_api.py issues list [--repo owner/name]
|
||||
python scripts/gitea_api.py issues create --repo owner/name --title TITLE [--body BODY]
|
||||
|
||||
Zero external dependencies (stdlib only: urllib, json, argparse, sys, os).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_URL = "https://git.shumengya.top"
|
||||
DEFAULT_TOKEN = "dbc5dfe55b19e9a32f685a97f126cafc2ef76449"
|
||||
DEFAULT_OWNER = "shumengya"
|
||||
|
||||
# Allow override via environment variables
|
||||
GITEA_URL = os.environ.get("GITEA_URL", DEFAULT_URL).rstrip("/")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", DEFAULT_TOKEN)
|
||||
API_BASE = f"{GITEA_URL}/api/v1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _headers() -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _request(
|
||||
method: str,
|
||||
path: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> tuple[int, Any]:
|
||||
"""Send HTTP request to Gitea API. Returns (status_code, parsed_json)."""
|
||||
url = f"{API_BASE}{path}"
|
||||
body = json.dumps(data).encode("utf-8") if data else None
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers=_headers(),
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
raw = resp.read()
|
||||
status = resp.status
|
||||
if raw.strip():
|
||||
return status, json.loads(raw.decode("utf-8"))
|
||||
return status, None
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
detail = json.loads(e.read().decode("utf-8"))
|
||||
except Exception:
|
||||
detail = {"message": str(e)}
|
||||
return e.code, detail
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[ERROR] Network error: {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_repos_list(_args: argparse.Namespace) -> None:
|
||||
status, data = _request("GET", f"/users/{DEFAULT_OWNER}/repos?limit=100")
|
||||
if status != 200:
|
||||
print(f"[ERROR] Failed to list repos (HTTP {status}): {data}")
|
||||
sys.exit(1)
|
||||
if not data:
|
||||
print("No repositories found.")
|
||||
return
|
||||
print(f"{'Name':<30} {'Private':<8} {'Description'}")
|
||||
print("-" * 80)
|
||||
for r in sorted(data, key=lambda x: x.get("name", "")):
|
||||
name = r.get("name", "?")
|
||||
private = "yes" if r.get("private") else "no"
|
||||
desc = (r.get("description") or "")[:45]
|
||||
print(f"{name:<30} {private:<8} {desc}")
|
||||
|
||||
|
||||
def cmd_repos_create(args: argparse.Namespace) -> None:
|
||||
payload: dict[str, Any] = {
|
||||
"name": args.name,
|
||||
"description": args.desc or "",
|
||||
"private": args.private,
|
||||
"auto_init": False,
|
||||
}
|
||||
status, data = _request("POST", "/user/repos", payload)
|
||||
if status in (201, 200):
|
||||
print(f"[OK] Repository created: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}")
|
||||
print(f" HTTPS: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}.git")
|
||||
print(f" SSH: ssh://git@{GITEA_URL.split('://')[1]}:8022/{DEFAULT_OWNER}/{args.name}.git")
|
||||
elif status == 409:
|
||||
print(f"[WARN] Repository '{args.name}' already exists.", file=sys.stderr)
|
||||
else:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_repos_delete(args: argparse.Namespace) -> None:
|
||||
print(f"Deleting repository {DEFAULT_OWNER}/{args.name}...", end=" ")
|
||||
status, data = _request("DELETE", f"/repos/{DEFAULT_OWNER}/{args.name}")
|
||||
if status in (204, 200):
|
||||
print("[OK] Deleted.")
|
||||
else:
|
||||
print(f"\n[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_repos_exists(args: argparse.Namespace) -> None:
|
||||
status, _data = _request("GET", f"/repos/{DEFAULT_OWNER}/{args.name}")
|
||||
if status == 200:
|
||||
print(f"[YES] Repository '{args.name}' exists.")
|
||||
elif status == 404:
|
||||
print(f"[NO] Repository '{args.name}' does not exist.")
|
||||
else:
|
||||
print(f"[UNKNOWN] HTTP {status}", file=sys.stderr)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Issue commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_issues_list(args: argparse.Namespace) -> None:
|
||||
repo = args.repo or f"{DEFAULT_OWNER}/sproutclaw"
|
||||
status, data = _request("GET", f"/repos/{repo}/issues?state={args.state}&limit=50")
|
||||
if status != 200:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not data:
|
||||
print(f"No {args.state} issues in {repo}.")
|
||||
return
|
||||
print(f"{'#':<6} {'State':<8} {'Title'}")
|
||||
print("-" * 70)
|
||||
for issue in data:
|
||||
# PRs also appear in issues endpoint; skip them
|
||||
if issue.get("pull_request"):
|
||||
continue
|
||||
num = issue.get("number", "?")
|
||||
state = issue.get("state", "?")
|
||||
title = issue.get("title", "?")
|
||||
print(f"#{num:<5} {state:<8} {title}")
|
||||
|
||||
|
||||
def cmd_issues_create(args: argparse.Namespace) -> None:
|
||||
if not args.repo:
|
||||
print("[ERROR] --repo is required (e.g. shumengya/sproutclaw)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
payload: dict[str, Any] = {"title": args.title}
|
||||
if args.body:
|
||||
payload["body"] = args.body
|
||||
status, data = _request("POST", f"/repos/{args.repo}/issues", payload)
|
||||
if status in (201, 200):
|
||||
url = data.get("html_url", "?")
|
||||
print(f"[OK] Issue created: {url}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Gitea API helper — 纯 Python 标准库,零外部依赖。"
|
||||
)
|
||||
sub = parser.add_subparsers(dest="resource", required=True)
|
||||
|
||||
# --- repos ---
|
||||
repos_parser = sub.add_parser("repos", help="仓库操作")
|
||||
repos_commands = repos_parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
# repos list
|
||||
repos_commands.add_parser("list", help="列出所有仓库")
|
||||
|
||||
# repos create
|
||||
rc = repos_commands.add_parser("create", help="创建新仓库")
|
||||
rc.add_argument("name", help="仓库名称")
|
||||
rc.add_argument("--desc", "-d", default="", help="仓库描述")
|
||||
rc.add_argument("--private", action="store_true", help="创建私有仓库")
|
||||
|
||||
# repos delete
|
||||
rd = repos_commands.add_parser("delete", help="删除仓库")
|
||||
rd.add_argument("name", help="仓库名称")
|
||||
|
||||
# repos exists
|
||||
re = repos_commands.add_parser("exists", help="检查仓库是否存在")
|
||||
re.add_argument("name", help="仓库名称")
|
||||
|
||||
# --- issues ---
|
||||
issues_parser = sub.add_parser("issues", help="Issue 操作")
|
||||
issues_commands = issues_parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
# issues list
|
||||
il = issues_commands.add_parser("list", help="列出 Issue")
|
||||
il.add_argument("--repo", "-r", help="仓库路径 (owner/name)")
|
||||
il.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
||||
|
||||
# issues create
|
||||
ic = issues_commands.add_parser("create", help="创建 Issue")
|
||||
ic.add_argument("--repo", "-r", required=True, help="仓库路径 (owner/name)")
|
||||
ic.add_argument("--title", "-t", required=True, help="Issue 标题")
|
||||
ic.add_argument("--body", "-b", default="", help="Issue 描述")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if not GITEA_TOKEN:
|
||||
print("[ERROR] GITEA_TOKEN environment variable is not set and no default available.")
|
||||
sys.exit(1)
|
||||
|
||||
dispatch = {
|
||||
("repos", "list"): cmd_repos_list,
|
||||
("repos", "create"): cmd_repos_create,
|
||||
("repos", "delete"): cmd_repos_delete,
|
||||
("repos", "exists"): cmd_repos_exists,
|
||||
("issues", "list"): cmd_issues_list,
|
||||
("issues", "create"): cmd_issues_create,
|
||||
}
|
||||
|
||||
key = (args.resource, args.action)
|
||||
if key in dispatch:
|
||||
dispatch[key](args)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user