yuque-toolkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .client import YuqueClient, load_dotenv
2
+
3
+ __all__ = ["YuqueClient", "load_dotenv"]
yuque_toolkit/cli.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ from .client import YuqueClient, load_dotenv
9
+
10
+
11
+ def add_repo_args(parser: argparse.ArgumentParser) -> None:
12
+ parser.add_argument(
13
+ "--group-login",
14
+ default=None,
15
+ help="Repo owner login, falls back to YUQUE_GROUP_LOGIN",
16
+ )
17
+ parser.add_argument(
18
+ "--book-slug",
19
+ default=None,
20
+ help="Repo slug, falls back to YUQUE_BOOK_SLUG",
21
+ )
22
+
23
+
24
+ def require_repo_args(args: argparse.Namespace) -> tuple[str, str]:
25
+ group_login = args.group_login or os.getenv("YUQUE_GROUP_LOGIN")
26
+ book_slug = args.book_slug or os.getenv("YUQUE_BOOK_SLUG")
27
+ if not group_login or not book_slug:
28
+ raise SystemExit(
29
+ "Repo-scoped command requires --group-login/--book-slug or env defaults"
30
+ )
31
+ return group_login, book_slug
32
+
33
+
34
+ def build_parser() -> argparse.ArgumentParser:
35
+ parser = argparse.ArgumentParser(description="Reusable Yuque API CLI")
36
+ parser.add_argument("--env-file", default=".env", help="Path to .env file")
37
+ parser.add_argument("--base-url", default=None, help="Override YUQUE_BASE_URL")
38
+ subparsers = parser.add_subparsers(dest="command", required=True)
39
+
40
+ subparsers.add_parser("me", help="Get current user")
41
+
42
+ repo_parser = subparsers.add_parser("repo", help="Get repo detail")
43
+ add_repo_args(repo_parser)
44
+
45
+ docs_parser = subparsers.add_parser("docs", help="List docs")
46
+ add_repo_args(docs_parser)
47
+ docs_parser.add_argument("--offset", type=int, default=0)
48
+ docs_parser.add_argument("--limit", type=int, default=20)
49
+
50
+ doc_parser = subparsers.add_parser("doc", help="Get doc detail")
51
+ add_repo_args(doc_parser)
52
+ doc_parser.add_argument("--doc-slug", required=True)
53
+ doc_parser.add_argument("--raw", type=int, default=0)
54
+
55
+ toc_parser = subparsers.add_parser("toc", help="Get repo toc")
56
+ add_repo_args(toc_parser)
57
+
58
+ update_toc_parser = subparsers.add_parser("update-toc", help="Update repo toc")
59
+ add_repo_args(update_toc_parser)
60
+ update_toc_parser.add_argument("--action", required=True)
61
+ update_toc_parser.add_argument("--action-mode")
62
+ update_toc_parser.add_argument("--uuid")
63
+ update_toc_parser.add_argument("--target-uuid")
64
+ update_toc_parser.add_argument("--doc-id", type=int)
65
+ update_toc_parser.add_argument("--doc-ids", nargs="*", type=int)
66
+ update_toc_parser.add_argument("--title")
67
+ update_toc_parser.add_argument("--url")
68
+ update_toc_parser.add_argument("--visible", type=int, choices=[0, 1])
69
+ update_toc_parser.add_argument("--open-window", type=int, choices=[0, 1])
70
+
71
+ create_parser = subparsers.add_parser("create-doc", help="Create doc")
72
+ add_repo_args(create_parser)
73
+ create_parser.add_argument("--title", required=True)
74
+ create_parser.add_argument("--body", required=True)
75
+ create_parser.add_argument("--slug")
76
+ create_parser.add_argument("--public", type=int, choices=[0, 1])
77
+ create_parser.add_argument("--format", default="markdown")
78
+
79
+ update_parser = subparsers.add_parser("update-doc", help="Update doc")
80
+ add_repo_args(update_parser)
81
+ update_parser.add_argument("--doc-slug", required=True)
82
+ update_parser.add_argument("--title")
83
+ update_parser.add_argument("--body")
84
+ update_parser.add_argument("--public", type=int, choices=[0, 1])
85
+ update_parser.add_argument("--format")
86
+ return parser
87
+
88
+
89
+ def main() -> int:
90
+ parser = build_parser()
91
+ args = parser.parse_args()
92
+ load_dotenv(args.env_file)
93
+
94
+ token = os.getenv("YUQUE_TOKEN", "")
95
+ base_url = args.base_url or os.getenv("YUQUE_BASE_URL", "https://www.yuque.com")
96
+ client = YuqueClient(token=token, base_url=base_url)
97
+
98
+ if args.command == "me":
99
+ result = client.get_user()
100
+ elif args.command == "repo":
101
+ group_login, book_slug = require_repo_args(args)
102
+ result = client.get_repo(group_login, book_slug)
103
+ elif args.command == "docs":
104
+ group_login, book_slug = require_repo_args(args)
105
+ result = client.list_docs(
106
+ group_login, book_slug, offset=args.offset, limit=args.limit
107
+ )
108
+ elif args.command == "doc":
109
+ group_login, book_slug = require_repo_args(args)
110
+ result = client.get_doc(group_login, book_slug, args.doc_slug, raw=args.raw)
111
+ elif args.command == "toc":
112
+ group_login, book_slug = require_repo_args(args)
113
+ result = client.get_toc(group_login, book_slug)
114
+ elif args.command == "create-doc":
115
+ group_login, book_slug = require_repo_args(args)
116
+ result = client.create_doc(
117
+ group_login,
118
+ book_slug,
119
+ title=args.title,
120
+ body=args.body,
121
+ slug=args.slug,
122
+ public=args.public,
123
+ format_=args.format,
124
+ )
125
+ elif args.command == "update-doc":
126
+ group_login, book_slug = require_repo_args(args)
127
+ result = client.update_doc(
128
+ group_login,
129
+ book_slug,
130
+ args.doc_slug,
131
+ title=args.title,
132
+ body=args.body,
133
+ public=args.public,
134
+ format_=args.format,
135
+ )
136
+ elif args.command == "update-toc":
137
+ group_login, book_slug = require_repo_args(args)
138
+ result = client.update_toc(
139
+ group_login,
140
+ book_slug,
141
+ action=args.action,
142
+ action_mode=args.action_mode,
143
+ uuid=args.uuid,
144
+ target_uuid=args.target_uuid,
145
+ doc_id=args.doc_id,
146
+ doc_ids=args.doc_ids,
147
+ title=args.title,
148
+ url=args.url,
149
+ visible=args.visible,
150
+ open_window=args.open_window,
151
+ )
152
+ else:
153
+ raise SystemExit(f"Unsupported command: {args.command}")
154
+
155
+ json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
156
+ sys.stdout.write("\n")
157
+ return 0
158
+
159
+
160
+ if __name__ == "__main__":
161
+ raise SystemExit(main())
@@ -0,0 +1,319 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+ from urllib import error, parse, request
8
+
9
+
10
+ def load_dotenv(path: str = ".env") -> None:
11
+ """从 `.env` 文件加载环境变量。
12
+
13
+ 参数:
14
+ - path: `.env` 文件路径,默认读取项目根目录下的 `.env`
15
+
16
+ 返回值:
17
+ - 无
18
+ """
19
+ env_path = Path(path)
20
+ if not env_path.exists():
21
+ return
22
+
23
+ for raw_line in env_path.read_text(encoding="utf-8").splitlines():
24
+ line = raw_line.strip()
25
+ if not line or line.startswith("#") or "=" not in line:
26
+ continue
27
+ key, value = line.split("=", 1)
28
+ os.environ.setdefault(key.strip(), value.strip().strip("'").strip('"'))
29
+
30
+
31
+ class YuqueClient:
32
+ """语雀 OpenAPI 的轻量 Python 客户端。
33
+
34
+ 参数:
35
+ - token: 语雀 `X-Auth-Token`
36
+ - base_url: 语雀服务地址,默认 `https://www.yuque.com`
37
+ """
38
+
39
+ def __init__(self, token: str, base_url: str = "https://www.yuque.com") -> None:
40
+ if not token:
41
+ raise ValueError("YUQUE_TOKEN is required")
42
+ self.base_url = base_url.rstrip("/")
43
+ self.token = token
44
+
45
+ def request(
46
+ self,
47
+ method: str,
48
+ path: str,
49
+ *,
50
+ params: Optional[Dict[str, Any]] = None,
51
+ data: Optional[Dict[str, Any]] = None,
52
+ json_data: Optional[Dict[str, Any]] = None,
53
+ ) -> Dict[str, Any]:
54
+ """发送底层 HTTP 请求到语雀 API。
55
+
56
+ 参数:
57
+ - method: HTTP 方法,例如 `GET`、`POST`、`PUT`
58
+ - path: 语雀 API 路径,例如 `/api/v2/user`
59
+ - params: URL 查询参数
60
+ - data: 表单请求体,使用 `application/x-www-form-urlencoded`
61
+ - json_data: JSON 请求体,使用 `application/json`
62
+
63
+ 返回值:
64
+ - 解析后的 JSON 字典
65
+ """
66
+ url = f"{self.base_url}{path}"
67
+ if params:
68
+ query = parse.urlencode(
69
+ {k: v for k, v in params.items() if v is not None and v != ""}
70
+ )
71
+ url = f"{url}?{query}"
72
+
73
+ payload = None
74
+ headers = {
75
+ "X-Auth-Token": self.token,
76
+ "User-Agent": "yuque-toolkit/0.1",
77
+ "Accept": "application/json",
78
+ }
79
+ if data is not None and json_data is not None:
80
+ raise ValueError("data and json_data cannot be used together")
81
+ if data is not None:
82
+ payload = parse.urlencode(data, doseq=True).encode("utf-8")
83
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
84
+ if json_data is not None:
85
+ payload = json.dumps(json_data, ensure_ascii=False).encode("utf-8")
86
+ headers["Content-Type"] = "application/json; charset=utf-8"
87
+
88
+ req = request.Request(url, data=payload, headers=headers, method=method.upper())
89
+ try:
90
+ with request.urlopen(req) as resp:
91
+ body = resp.read().decode("utf-8")
92
+ return json.loads(body) if body else {}
93
+ except error.HTTPError as exc:
94
+ body = exc.read().decode("utf-8", errors="replace")
95
+ raise RuntimeError(
96
+ f"Yuque API error {exc.code} {exc.reason}: {body}"
97
+ ) from exc
98
+ except error.URLError as exc:
99
+ raise RuntimeError(f"Yuque API request failed: {exc.reason}") from exc
100
+
101
+ def get_user(self) -> Dict[str, Any]:
102
+ """获取当前 Token 对应的用户信息。
103
+
104
+ 参数:
105
+ - 无
106
+
107
+ 返回值:
108
+ - 语雀用户信息字典,通常位于响应的 `data` 字段中
109
+ """
110
+ return self.request("GET", "/api/v2/user")
111
+
112
+ def get_repo(self, namespace: str, repo_slug: str) -> Dict[str, Any]:
113
+ """获取指定知识库的详情。
114
+
115
+ 参数:
116
+ - namespace: 知识库所属用户或团队登录名
117
+ - repo_slug: 知识库 slug
118
+
119
+ 返回值:
120
+ - 知识库详情字典
121
+ """
122
+ return self.request("GET", f"/api/v2/repos/{namespace}/{repo_slug}")
123
+
124
+ def list_docs(
125
+ self,
126
+ namespace: str,
127
+ repo_slug: str,
128
+ *,
129
+ offset: int = 0,
130
+ limit: int = 20,
131
+ ) -> Dict[str, Any]:
132
+ """分页获取知识库下的文档列表。
133
+
134
+ 参数:
135
+ - namespace: 知识库所属用户或团队登录名
136
+ - repo_slug: 知识库 slug
137
+ - offset: 分页偏移量
138
+ - limit: 单次返回数量
139
+
140
+ 返回值:
141
+ - 文档列表响应字典
142
+ """
143
+ return self.request(
144
+ "GET",
145
+ f"/api/v2/repos/{namespace}/{repo_slug}/docs",
146
+ params={"offset": offset, "limit": limit},
147
+ )
148
+
149
+ def get_doc(
150
+ self,
151
+ namespace: str,
152
+ repo_slug: str,
153
+ doc_slug: str,
154
+ *,
155
+ raw: int = 0,
156
+ ) -> Dict[str, Any]:
157
+ """获取单篇文档详情。
158
+
159
+ 参数:
160
+ - namespace: 知识库所属用户或团队登录名
161
+ - repo_slug: 知识库 slug
162
+ - doc_slug: 文档 slug
163
+ - raw: 是否返回原始内容,通常 `0` 为默认格式,`1` 为原始正文
164
+
165
+ 返回值:
166
+ - 文档详情字典
167
+ """
168
+ return self.request(
169
+ "GET",
170
+ f"/api/v2/repos/{namespace}/{repo_slug}/docs/{doc_slug}",
171
+ params={"raw": raw},
172
+ )
173
+
174
+ def create_doc(
175
+ self,
176
+ namespace: str,
177
+ repo_slug: str,
178
+ *,
179
+ title: str,
180
+ body: str,
181
+ slug: Optional[str] = None,
182
+ public: Optional[int] = None,
183
+ format_: str = "markdown",
184
+ ) -> Dict[str, Any]:
185
+ """在指定知识库中创建文档。
186
+
187
+ 参数:
188
+ - namespace: 知识库所属用户或团队登录名
189
+ - repo_slug: 知识库 slug
190
+ - title: 文档标题
191
+ - body: 文档正文
192
+ - slug: 文档 slug,可选
193
+ - public: 可见性,可选,常见为 `0` 或 `1`
194
+ - format_: 文档格式,默认 `markdown`
195
+
196
+ 返回值:
197
+ - 新建文档后的响应字典
198
+ """
199
+ return self.request(
200
+ "POST",
201
+ f"/api/v2/repos/{namespace}/{repo_slug}/docs",
202
+ data={
203
+ "title": title,
204
+ "slug": slug,
205
+ "public": public,
206
+ "format": format_,
207
+ "body": body,
208
+ },
209
+ )
210
+
211
+ def update_doc(
212
+ self,
213
+ namespace: str,
214
+ repo_slug: str,
215
+ doc_slug: str,
216
+ *,
217
+ title: Optional[str] = None,
218
+ body: Optional[str] = None,
219
+ public: Optional[int] = None,
220
+ format_: Optional[str] = None,
221
+ ) -> Dict[str, Any]:
222
+ """更新指定文档的内容或属性。
223
+
224
+ 参数:
225
+ - namespace: 知识库所属用户或团队登录名
226
+ - repo_slug: 知识库 slug
227
+ - doc_slug: 文档 slug
228
+ - title: 新标题,可选
229
+ - body: 新正文,可选
230
+ - public: 新可见性,可选
231
+ - format_: 新格式,可选
232
+
233
+ 返回值:
234
+ - 更新后的文档响应字典
235
+ """
236
+ payload = {
237
+ "title": title,
238
+ "body": body,
239
+ "public": public,
240
+ "format": format_,
241
+ }
242
+ return self.request(
243
+ "PUT",
244
+ f"/api/v2/repos/{namespace}/{repo_slug}/docs/{doc_slug}",
245
+ data={k: v for k, v in payload.items() if v is not None},
246
+ )
247
+
248
+ def get_toc(self, namespace: str, repo_slug: str) -> Dict[str, Any]:
249
+ """获取知识库目录树。
250
+
251
+ 参数:
252
+ - namespace: 知识库所属用户或团队登录名
253
+ - repo_slug: 知识库 slug
254
+
255
+ 返回值:
256
+ - 目录结构响应字典
257
+ """
258
+ return self.request("GET", f"/api/v2/repos/{namespace}/{repo_slug}/toc")
259
+
260
+ def update_toc(
261
+ self,
262
+ namespace: str,
263
+ repo_slug: str,
264
+ *,
265
+ action: str,
266
+ action_mode: Optional[str] = None,
267
+ uuid: Optional[str] = None,
268
+ target_uuid: Optional[str] = None,
269
+ doc_id: Optional[int] = None,
270
+ doc_ids: Optional[list[int]] = None,
271
+ title: Optional[str] = None,
272
+ url: Optional[str] = None,
273
+ visible: Optional[int] = None,
274
+ open_window: Optional[int] = None,
275
+ ) -> Dict[str, Any]:
276
+ """调用知识库目录写接口。
277
+
278
+ 参数:
279
+ - namespace: 知识库所属用户或团队登录名
280
+ - repo_slug: 知识库 slug
281
+ - action: 目录操作类型,例如 `appendByDocs`、`moveAfter`、`syncDoc`
282
+ - action_mode: 目录操作模式,常见为 `sibling` 或 `child`
283
+ - uuid: 当前目录节点 uuid
284
+ - target_uuid: 目标目录节点 uuid
285
+ - doc_id: 单个文档 id
286
+ - doc_ids: 批量文档 id 列表
287
+ - title: 目录节点标题
288
+ - url: 目录节点链接
289
+ - visible: 节点是否可见
290
+ - open_window: 是否新窗口打开
291
+
292
+ 返回值:
293
+ - 目录更新后的响应字典
294
+ """
295
+ payload: Dict[str, Any] = {"action": action}
296
+ if action_mode:
297
+ payload["action_mode"] = action_mode
298
+ if uuid:
299
+ payload["uuid"] = uuid
300
+ if target_uuid:
301
+ payload["target_uuid"] = target_uuid
302
+ if doc_id is not None:
303
+ payload["doc_id"] = doc_id
304
+ if doc_ids:
305
+ payload["doc_ids"] = doc_ids
306
+ if title is not None:
307
+ payload["title"] = title
308
+ if url is not None:
309
+ payload["url"] = url
310
+ if visible is not None:
311
+ payload["visible"] = visible
312
+ if open_window is not None:
313
+ payload["open_window"] = open_window
314
+
315
+ return self.request(
316
+ "PUT",
317
+ f"/api/v2/repos/{namespace}/{repo_slug}/toc",
318
+ json_data=payload,
319
+ )
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: yuque-toolkit
3
+ Version: 0.1.0
4
+ Summary: Reusable Python toolkit for Yuque OpenAPI
5
+ Author: fenyuan
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # Yuque Toolkit
17
+
18
+ `yuque-toolkit` 是一个面向 Python 项目的语雀 SDK,封装了常用的语雀 OpenAPI 调用能力,适合在自动化脚本、内容同步任务和内部业务系统中复用。
19
+
20
+ ## Features
21
+
22
+ - Get current user info
23
+ - Get repository detail
24
+ - List repository documents
25
+ - Get document detail
26
+ - Create document
27
+ - Update document
28
+ - Get repository TOC
29
+ - Update repository TOC
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install yuque-toolkit
35
+ ```
36
+
37
+ ## Environment Variables
38
+
39
+ ```env
40
+ YUQUE_BASE_URL=https://www.yuque.com
41
+ YUQUE_TOKEN=your_token_here
42
+ YUQUE_GROUP_LOGIN=your_group_or_user_login
43
+ YUQUE_BOOK_SLUG=your_book_slug
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import os
50
+
51
+ from yuque_toolkit import YuqueClient, load_dotenv
52
+
53
+ load_dotenv()
54
+
55
+ client = YuqueClient(
56
+ token=os.getenv("YUQUE_TOKEN", ""),
57
+ base_url=os.getenv("YUQUE_BASE_URL", "https://www.yuque.com"),
58
+ )
59
+
60
+ result = client.list_docs(
61
+ os.getenv("YUQUE_GROUP_LOGIN", ""),
62
+ os.getenv("YUQUE_BOOK_SLUG", ""),
63
+ limit=10,
64
+ )
65
+
66
+ print(result)
67
+ ```
68
+
69
+ ## CLI
70
+
71
+ ```bash
72
+ yuque-toolkit me
73
+ yuque-toolkit repo
74
+ yuque-toolkit docs --limit 10
75
+ yuque-toolkit toc
76
+ ```
@@ -0,0 +1,9 @@
1
+ yuque_toolkit/__init__.py,sha256=s5RaKfeDyCfPR4mNgp5SeVkXfw3RouEaSeYJXHAmIU4,87
2
+ yuque_toolkit/cli.py,sha256=VQlc0vxwva6ZMKsI_q6lXDWAZcWoZplLTkmEIsC1fhE,5886
3
+ yuque_toolkit/client.py,sha256=aMABSuSzlnnPE3ZjcAf0Nxr747uNvKKrOvsr8qYeGko,9867
4
+ yuque_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=t2sKxHnNmTb8EafD-jwd7etSH49rCQoCeST7sK54v84,1064
5
+ yuque_toolkit-0.1.0.dist-info/METADATA,sha256=wBsued6hE-YibaR2sUlVRIc8PnjzbXjn9iGsnvmiR3g,1593
6
+ yuque_toolkit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ yuque_toolkit-0.1.0.dist-info/entry_points.txt,sha256=aJeRW6CGAnKW7xDGDlqV4ZE4uiQeLWl7-v-Xd8fWrXs,57
8
+ yuque_toolkit-0.1.0.dist-info/top_level.txt,sha256=IVahidZvIDA_2Ma17orgOlQb8J_3rRZuXYZF4M3btPA,14
9
+ yuque_toolkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ yuque-toolkit = yuque_toolkit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fenyuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ yuque_toolkit