yuque-cli 0.2.0__tar.gz → 1.0.0__tar.gz
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.
- yuque_cli-1.0.0/.claude/settings.json +15 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/PKG-INFO +3 -3
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/README.md +2 -2
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/pyproject.toml +1 -1
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/cli.py +15 -14
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/urls.py +21 -5
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_cli.py +5 -5
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_urls.py +33 -9
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/uv.lock +1 -1
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/.claude/settings.local.json +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/.gitignore +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/.python-version +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/.vscode/extensions.json +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/.vscode/settings.json +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/AGENTS.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/CLAUDE.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/CONTEXT.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/docs/adr/0001-use-internal-web-api-with-cookie-auth.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/docs/adr/0002-cookie-acquisition-cdp-with-manual-fallback.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/docs/adr/0003-doc-update-fetch-then-merge.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/docs/internal-api.md +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/main.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/mise.toml +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/__init__.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/__main__.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/appdata.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/auth.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/client.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/config.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/errors.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/inputs.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/output.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/src/yuque_cli/session.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_appdata.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_auth.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_client.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_config.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_inputs.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_output.py +0 -0
- {yuque_cli-0.2.0 → yuque_cli-1.0.0}/test/test_session.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "EDIT|Write",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "if git diff --name-only HEAD 2>/dev/null | grep -q '^src/\\|^pyproject.toml'; then current=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = \"\\(.*\\)\"/\\1/'); IFS='.' read -r major minor patch <<< \"$current\"; new=\"$major.$minor.$((patch + 1))\"; sed -i '' \"s/^version = \\\".*\\\"/version = \\\"$new\\\"/\" pyproject.toml && echo \"[版本升级] $current → $new (patch)\"; fi"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yuque-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: 针对语雀的命令行客户端:cookie 认证,驱动内部 web 接口,提供文档与评论的 CRUD
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
@@ -101,12 +101,12 @@ yuque -y doc delete team/handbook/old-draft
|
|
|
101
101
|
| 命令 | 说明 |
|
|
102
102
|
| --- | --- |
|
|
103
103
|
| `comment list <space>/<repo>/<slug>` | 列出评论 |
|
|
104
|
-
| `comment
|
|
104
|
+
| `comment create <space>/<repo>/<slug> [-b/--body \| -F/--file]` | 添加评论 |
|
|
105
105
|
| `comment delete <comment_id>` | 删除评论 |
|
|
106
106
|
|
|
107
107
|
## 正文输入
|
|
108
108
|
|
|
109
|
-
`doc create` / `comment
|
|
109
|
+
`doc create` / `comment create` 正文来源(`--body` 与 `--file` 互斥):
|
|
110
110
|
|
|
111
111
|
1. `-b/--body` 内联字符串
|
|
112
112
|
2. `-F/--file` 从文件读(`-F -` 读 stdin)
|
|
@@ -91,12 +91,12 @@ yuque -y doc delete team/handbook/old-draft
|
|
|
91
91
|
| 命令 | 说明 |
|
|
92
92
|
| --- | --- |
|
|
93
93
|
| `comment list <space>/<repo>/<slug>` | 列出评论 |
|
|
94
|
-
| `comment
|
|
94
|
+
| `comment create <space>/<repo>/<slug> [-b/--body \| -F/--file]` | 添加评论 |
|
|
95
95
|
| `comment delete <comment_id>` | 删除评论 |
|
|
96
96
|
|
|
97
97
|
## 正文输入
|
|
98
98
|
|
|
99
|
-
`doc create` / `comment
|
|
99
|
+
`doc create` / `comment create` 正文来源(`--body` 与 `--file` 互斥):
|
|
100
100
|
|
|
101
101
|
1. `-b/--body` 内联字符串
|
|
102
102
|
2. `-F/--file` 从文件读(`-F -` 读 stdin)
|
|
@@ -86,7 +86,7 @@ def cli_errors():
|
|
|
86
86
|
raise typer.Exit(1)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def build_client(state: AppState) -> Client:
|
|
89
|
+
def build_client(state: AppState, host_hint: str | None = None) -> Client:
|
|
90
90
|
sess = session.load(config.session_file())
|
|
91
91
|
hooks: dict[str, list[Callable[..., Any]]] = {}
|
|
92
92
|
if state.verbose:
|
|
@@ -99,7 +99,8 @@ def build_client(state: AppState) -> Client:
|
|
|
99
99
|
# trust_env=False:默认直连,绕开 *_proxy 环境变量(如本地 Clash 的 socks5 代理)——
|
|
100
100
|
# 避免请求被本地代理拦截,也免去 SOCKS 需 socksio 的崩溃。如需经代理访问,移除此参数并装 httpx[socks]。
|
|
101
101
|
http = httpx.Client(timeout=30.0, event_hooks=hooks, trust_env=False)
|
|
102
|
-
|
|
102
|
+
# --host 显式指定优先;否则从 URL 提取的 host 作为 fallback
|
|
103
|
+
return Client(http, sess, base_url=config.base_url(state.host or host_hint))
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
def _pager(text: str) -> None:
|
|
@@ -320,7 +321,7 @@ def repo_get(
|
|
|
320
321
|
state: AppState = ctx.obj
|
|
321
322
|
with cli_errors():
|
|
322
323
|
ref = _resolve_repo_ref(url, space, repo)
|
|
323
|
-
client = build_client(state)
|
|
324
|
+
client = build_client(state, host_hint=ref.host)
|
|
324
325
|
repo_id = client.resolve_repo_id(ref.space, ref.repo)
|
|
325
326
|
data = client.repo_overview(repo_id)
|
|
326
327
|
docs = client.repo_docs(repo_id)
|
|
@@ -351,7 +352,7 @@ def doc_list(ctx: typer.Context, url: UrlArg) -> None:
|
|
|
351
352
|
with cli_errors():
|
|
352
353
|
assert url is not None
|
|
353
354
|
ref = parse_repo_ref(url)
|
|
354
|
-
client = build_client(state)
|
|
355
|
+
client = build_client(state, host_hint=ref.host)
|
|
355
356
|
repo_id = client.resolve_repo_id(ref.space, ref.repo)
|
|
356
357
|
docs = client.repo_docs(repo_id)
|
|
357
358
|
_emit(state, docs, output.render_docs(docs))
|
|
@@ -364,7 +365,7 @@ def doc_get(ctx: typer.Context, url: UrlArg) -> None:
|
|
|
364
365
|
with cli_errors():
|
|
365
366
|
assert url is not None
|
|
366
367
|
ref = parse_doc_ref(url)
|
|
367
|
-
client = build_client(state)
|
|
368
|
+
client = build_client(state, host_hint=ref.host)
|
|
368
369
|
if state.json:
|
|
369
370
|
app_data = client.doc_page_app_data(ref.space, ref.repo, ref.slug)
|
|
370
371
|
detail = client.doc_detail(ref.slug, appdata_repo_id(app_data))
|
|
@@ -404,13 +405,13 @@ def doc_create(
|
|
|
404
405
|
raise YuqueError("--title 为必填项")
|
|
405
406
|
ref = _resolve_repo_ref(url, space, repo)
|
|
406
407
|
text = _read_body(body, file)
|
|
407
|
-
client = build_client(state)
|
|
408
|
+
client = build_client(state, host_hint=ref.host)
|
|
408
409
|
repo_id = client.resolve_repo_id(ref.space, ref.repo)
|
|
409
410
|
pub = None if public is None else (1 if public else 0)
|
|
410
411
|
created = client.create_doc(
|
|
411
412
|
repo_id=repo_id, title=title, body=text, slug=slug, public=pub
|
|
412
413
|
)
|
|
413
|
-
url_out = f"{config.base_url(state.host)}/{ref.space}/{ref.repo}/{created.get('slug', '')}"
|
|
414
|
+
url_out = f"{config.base_url(state.host or ref.host)}/{ref.space}/{ref.repo}/{created.get('slug', '')}"
|
|
414
415
|
_emit(state, created, f"已创建:{url_out}")
|
|
415
416
|
|
|
416
417
|
|
|
@@ -433,7 +434,7 @@ def doc_update(
|
|
|
433
434
|
"没有要更新的字段(至少给 --title / 正文 / --public|--private 之一)"
|
|
434
435
|
)
|
|
435
436
|
ref = _resolve_doc_ref(url, space, repo)
|
|
436
|
-
client = build_client(state)
|
|
437
|
+
client = build_client(state, host_hint=ref.host)
|
|
437
438
|
app_data = client.doc_page_app_data(ref.space, ref.repo, ref.slug)
|
|
438
439
|
doc_id = appdata_doc_id(app_data)
|
|
439
440
|
meta = app_data.get("doc", {})
|
|
@@ -446,7 +447,7 @@ def doc_update(
|
|
|
446
447
|
updated = client.update_doc(
|
|
447
448
|
doc_id=doc_id, title=new_title, body=new_body, public=new_public
|
|
448
449
|
)
|
|
449
|
-
url_out = f"{config.base_url(state.host)}/{ref.space}/{ref.repo}/{ref.slug}"
|
|
450
|
+
url_out = f"{config.base_url(state.host or ref.host)}/{ref.space}/{ref.repo}/{ref.slug}"
|
|
450
451
|
_emit(state, updated, f"已更新:{url_out}")
|
|
451
452
|
|
|
452
453
|
|
|
@@ -457,7 +458,7 @@ def doc_delete(ctx: typer.Context, url: UrlArg) -> None:
|
|
|
457
458
|
with cli_errors():
|
|
458
459
|
assert url is not None
|
|
459
460
|
ref = parse_doc_ref(url)
|
|
460
|
-
client = build_client(state)
|
|
461
|
+
client = build_client(state, host_hint=ref.host)
|
|
461
462
|
app_data = client.doc_page_app_data(ref.space, ref.repo, ref.slug)
|
|
462
463
|
doc_id = appdata_doc_id(app_data)
|
|
463
464
|
title = app_data.get("doc", {}).get("title", ref.slug)
|
|
@@ -476,7 +477,7 @@ def comment_list(ctx: typer.Context, url: UrlArg) -> None:
|
|
|
476
477
|
with cli_errors():
|
|
477
478
|
assert url is not None
|
|
478
479
|
ref = parse_doc_ref(url)
|
|
479
|
-
client = build_client(state)
|
|
480
|
+
client = build_client(state, host_hint=ref.host)
|
|
480
481
|
_, doc_id = client.resolve_doc_ids(ref.space, ref.repo, ref.slug)
|
|
481
482
|
items = client.comments(doc_id)
|
|
482
483
|
_emit(state, items, output.render_comments(items))
|
|
@@ -488,8 +489,8 @@ SelectionOpt = Annotated[
|
|
|
488
489
|
]
|
|
489
490
|
|
|
490
491
|
|
|
491
|
-
@comment_app.command("
|
|
492
|
-
def
|
|
492
|
+
@comment_app.command("create")
|
|
493
|
+
def comment_create(
|
|
493
494
|
ctx: typer.Context,
|
|
494
495
|
url: UrlArg,
|
|
495
496
|
body: BodyOpt = None,
|
|
@@ -504,7 +505,7 @@ def comment_add(
|
|
|
504
505
|
text = _read_body(body, file)
|
|
505
506
|
if not text:
|
|
506
507
|
raise YuqueError("评论正文不能为空(用 --body 或 --file/-)")
|
|
507
|
-
client = build_client(state)
|
|
508
|
+
client = build_client(state, host_hint=ref.host)
|
|
508
509
|
_, doc_id = client.resolve_doc_ids(ref.space, ref.repo, ref.slug)
|
|
509
510
|
if selection is not None:
|
|
510
511
|
try:
|
|
@@ -12,6 +12,7 @@ class UrlParseError(ValueError):
|
|
|
12
12
|
class RepoRef:
|
|
13
13
|
space: str
|
|
14
14
|
repo: str
|
|
15
|
+
host: str | None = None
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@dataclass(frozen=True)
|
|
@@ -19,10 +20,11 @@ class DocRef:
|
|
|
19
20
|
space: str
|
|
20
21
|
repo: str
|
|
21
22
|
slug: str
|
|
23
|
+
host: str | None = None
|
|
22
24
|
|
|
23
25
|
@property
|
|
24
26
|
def repo_ref(self) -> RepoRef:
|
|
25
|
-
return RepoRef(self.space, self.repo)
|
|
27
|
+
return RepoRef(self.space, self.repo, host=self.host)
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def _segments(raw: str) -> list[str]:
|
|
@@ -42,14 +44,28 @@ def _segments(raw: str) -> list[str]:
|
|
|
42
44
|
return [seg for seg in path.split("/") if seg]
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
def extract_host(raw: str) -> str | None:
|
|
48
|
+
"""从完整 URL 或带 host 的路径中提取 host;无法识别时返回 None。"""
|
|
49
|
+
s = raw.strip()
|
|
50
|
+
if not s:
|
|
51
|
+
return None
|
|
52
|
+
if "://" in s:
|
|
53
|
+
return urlsplit(s).hostname
|
|
54
|
+
# 无 scheme:首段含 "." 即视为 host
|
|
55
|
+
parts = s.split("/")
|
|
56
|
+
if parts and "." in parts[0]:
|
|
57
|
+
return parts[0]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
45
61
|
def parse_doc_ref(raw: str) -> DocRef:
|
|
46
62
|
segs = _segments(raw)
|
|
47
|
-
if len(segs)
|
|
63
|
+
if len(segs) < 3:
|
|
48
64
|
raise UrlParseError(
|
|
49
65
|
f"文档地址应形如 {{space}}/{{repo}}/{{slug}}(三段),得到:{raw!r}"
|
|
50
66
|
)
|
|
51
|
-
space, repo, slug = segs
|
|
52
|
-
return DocRef(space=space, repo=repo, slug=slug)
|
|
67
|
+
space, repo, slug = segs[:3]
|
|
68
|
+
return DocRef(space=space, repo=repo, slug=slug, host=extract_host(raw))
|
|
53
69
|
|
|
54
70
|
|
|
55
71
|
def parse_repo_ref(raw: str) -> RepoRef:
|
|
@@ -60,4 +76,4 @@ def parse_repo_ref(raw: str) -> RepoRef:
|
|
|
60
76
|
)
|
|
61
77
|
if len(segs) > 3:
|
|
62
78
|
raise UrlParseError(f"地址段数过多,无法识别为知识库:{raw!r}")
|
|
63
|
-
return RepoRef(space=segs[0], repo=segs[1])
|
|
79
|
+
return RepoRef(space=segs[0], repo=segs[1], host=extract_host(raw))
|
|
@@ -99,7 +99,7 @@ class FakeClient:
|
|
|
99
99
|
@pytest.fixture
|
|
100
100
|
def fake(monkeypatch):
|
|
101
101
|
fc = FakeClient()
|
|
102
|
-
monkeypatch.setattr(cli, "build_client", lambda state: fc)
|
|
102
|
+
monkeypatch.setattr(cli, "build_client", lambda state, host_hint=None: fc)
|
|
103
103
|
return fc
|
|
104
104
|
|
|
105
105
|
|
|
@@ -374,20 +374,20 @@ class TestComment:
|
|
|
374
374
|
|
|
375
375
|
def test_add(self, fake):
|
|
376
376
|
result = runner.invoke(
|
|
377
|
-
cli.app, ["comment", "
|
|
377
|
+
cli.app, ["comment", "create", "lg/bk/sl", "--body", "nice"]
|
|
378
378
|
)
|
|
379
379
|
assert "已评论" in result.output
|
|
380
380
|
assert calls(fake, "create_comment")[0][1] == {"doc_id": 88, "body": "nice"}
|
|
381
381
|
|
|
382
382
|
def test_add_empty_body_errors(self, fake):
|
|
383
|
-
result = runner.invoke(cli.app, ["comment", "
|
|
383
|
+
result = runner.invoke(cli.app, ["comment", "create", "lg/bk/sl"])
|
|
384
384
|
assert result.exit_code == 1
|
|
385
385
|
assert "评论正文不能为空" in result.output
|
|
386
386
|
|
|
387
387
|
def test_add_with_selection(self, fake):
|
|
388
388
|
sel = '{"paragraph_id":"u-pid","text":"hello","text_offset":0,"selection_range":{"start":{"id":"u-eid","text":"hello","offset":0,"paragraphId":"u-pid","paragraphOffset":0},"end":{"id":"u-eid","text":"hello","offset":3,"paragraphId":"u-pid","paragraphOffset":3}},"doc_version_id":123}'
|
|
389
389
|
result = runner.invoke(
|
|
390
|
-
cli.app, ["comment", "
|
|
390
|
+
cli.app, ["comment", "create", "lg/bk/sl", "--body", "nice", "--selection", sel]
|
|
391
391
|
)
|
|
392
392
|
assert "已评论" in result.output
|
|
393
393
|
kw = calls(fake, "create_comment")[0][1]
|
|
@@ -406,7 +406,7 @@ class TestComment:
|
|
|
406
406
|
|
|
407
407
|
def test_add_with_selection_invalid_json_errors(self, fake):
|
|
408
408
|
result = runner.invoke(
|
|
409
|
-
cli.app, ["comment", "
|
|
409
|
+
cli.app, ["comment", "create", "lg/bk/sl", "--body", "nice", "--selection", "not json"]
|
|
410
410
|
)
|
|
411
411
|
assert result.exit_code == 1
|
|
412
412
|
assert "--selection JSON 解析失败" in result.output
|
|
@@ -4,6 +4,7 @@ from yuque_cli.urls import (
|
|
|
4
4
|
RepoRef,
|
|
5
5
|
DocRef,
|
|
6
6
|
UrlParseError,
|
|
7
|
+
extract_host,
|
|
7
8
|
parse_repo_ref,
|
|
8
9
|
parse_doc_ref,
|
|
9
10
|
)
|
|
@@ -12,15 +13,15 @@ from yuque_cli.urls import (
|
|
|
12
13
|
class TestParseDocRef:
|
|
13
14
|
def test_full_https_url(self):
|
|
14
15
|
ref = parse_doc_ref("https://www.yuque.com/abc/team-book/my-slug")
|
|
15
|
-
assert ref == DocRef(space="abc", repo="team-book", slug="my-slug")
|
|
16
|
+
assert ref == DocRef(space="abc", repo="team-book", slug="my-slug", host="www.yuque.com")
|
|
16
17
|
|
|
17
18
|
def test_http_scheme(self):
|
|
18
19
|
ref = parse_doc_ref("http://www.yuque.com/abc/book/slug")
|
|
19
|
-
assert ref == DocRef("abc", "book", "slug")
|
|
20
|
+
assert ref == DocRef("abc", "book", "slug", host="www.yuque.com")
|
|
20
21
|
|
|
21
22
|
def test_host_without_scheme(self):
|
|
22
23
|
ref = parse_doc_ref("www.yuque.com/abc/book/slug")
|
|
23
|
-
assert ref == DocRef("abc", "book", "slug")
|
|
24
|
+
assert ref == DocRef("abc", "book", "slug", host="www.yuque.com")
|
|
24
25
|
|
|
25
26
|
def test_path_only(self):
|
|
26
27
|
ref = parse_doc_ref("abc/book/slug")
|
|
@@ -34,7 +35,7 @@ class TestParseDocRef:
|
|
|
34
35
|
|
|
35
36
|
def test_strips_query_and_fragment(self):
|
|
36
37
|
ref = parse_doc_ref("https://www.yuque.com/abc/book/slug?from=x#h1")
|
|
37
|
-
assert ref == DocRef("abc", "book", "slug")
|
|
38
|
+
assert ref == DocRef("abc", "book", "slug", host="www.yuque.com")
|
|
38
39
|
|
|
39
40
|
def test_strips_surrounding_whitespace(self):
|
|
40
41
|
assert parse_doc_ref(" abc/book/slug ") == DocRef("abc", "book", "slug")
|
|
@@ -51,9 +52,12 @@ class TestParseDocRef:
|
|
|
51
52
|
with pytest.raises(UrlParseError):
|
|
52
53
|
parse_doc_ref("")
|
|
53
54
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
def test_extra_segments_use_first_three(self):
|
|
56
|
+
"""多余的路径段(如 /edit)会被忽略,只取前三段。"""
|
|
57
|
+
assert parse_doc_ref("abc/book/slug/edit") == DocRef("abc", "book", "slug")
|
|
58
|
+
assert parse_doc_ref(
|
|
59
|
+
"https://www.yuque.com/abc/book/slug/edit"
|
|
60
|
+
) == DocRef("abc", "book", "slug", host="www.yuque.com")
|
|
57
61
|
|
|
58
62
|
def test_repo_ref_property(self):
|
|
59
63
|
ref = parse_doc_ref("abc/book/slug")
|
|
@@ -63,7 +67,7 @@ class TestParseDocRef:
|
|
|
63
67
|
class TestParseRepoRef:
|
|
64
68
|
def test_full_https_url(self):
|
|
65
69
|
assert parse_repo_ref("https://www.yuque.com/abc/team-book") == RepoRef(
|
|
66
|
-
"abc", "team-book"
|
|
70
|
+
"abc", "team-book", host="www.yuque.com"
|
|
67
71
|
)
|
|
68
72
|
|
|
69
73
|
def test_path_only(self):
|
|
@@ -73,7 +77,7 @@ class TestParseRepoRef:
|
|
|
73
77
|
assert parse_repo_ref("abc/team-book/some-slug") == RepoRef("abc", "team-book")
|
|
74
78
|
|
|
75
79
|
def test_host_stripped_and_trailing_slash(self):
|
|
76
|
-
assert parse_repo_ref("www.yuque.com/abc/book/") == RepoRef("abc", "book")
|
|
80
|
+
assert parse_repo_ref("www.yuque.com/abc/book/") == RepoRef("abc", "book", host="www.yuque.com")
|
|
77
81
|
|
|
78
82
|
def test_space_only_is_rejected(self):
|
|
79
83
|
with pytest.raises(UrlParseError):
|
|
@@ -82,3 +86,23 @@ class TestParseRepoRef:
|
|
|
82
86
|
def test_empty_is_rejected(self):
|
|
83
87
|
with pytest.raises(UrlParseError):
|
|
84
88
|
parse_repo_ref(" ")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestExtractHost:
|
|
92
|
+
def test_full_url(self):
|
|
93
|
+
assert extract_host("https://www.yuque.com/abc/book/slug") == "www.yuque.com"
|
|
94
|
+
|
|
95
|
+
def test_custom_domain(self):
|
|
96
|
+
assert extract_host("https://jlpay.yuque.com/pmw77l/iybc48/doc") == "jlpay.yuque.com"
|
|
97
|
+
|
|
98
|
+
def test_no_scheme_with_host(self):
|
|
99
|
+
assert extract_host("jlpay.yuque.com/abc/book/slug") == "jlpay.yuque.com"
|
|
100
|
+
|
|
101
|
+
def test_path_only(self):
|
|
102
|
+
assert extract_host("abc/book/slug") is None
|
|
103
|
+
|
|
104
|
+
def test_empty(self):
|
|
105
|
+
assert extract_host("") is None
|
|
106
|
+
|
|
107
|
+
def test_whitespace(self):
|
|
108
|
+
assert extract_host(" https://foo.com/a/b ") == "foo.com"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{yuque_cli-0.2.0 → yuque_cli-1.0.0}/docs/adr/0002-cookie-acquisition-cdp-with-manual-fallback.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|