parsehub 2.0.19__tar.gz → 2.0.20__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.
- {parsehub-2.0.19/src/parsehub.egg-info → parsehub-2.0.20}/PKG-INFO +1 -2
- {parsehub-2.0.19 → parsehub-2.0.20}/pyproject.toml +1 -2
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/cli.py +71 -29
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/cli_config.py +6 -63
- {parsehub-2.0.19 → parsehub-2.0.20/src/parsehub.egg-info}/PKG-INFO +1 -2
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub.egg-info/requires.txt +0 -1
- {parsehub-2.0.19 → parsehub-2.0.20}/test/test_cli.py +59 -27
- parsehub-2.0.20/test/test_cli_config.py +49 -0
- parsehub-2.0.19/test/test_cli_config.py +0 -79
- {parsehub-2.0.19 → parsehub-2.0.20}/LICENSE +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/README.md +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/setup.cfg +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/config/config.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/errors.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/bilibili.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/instagram.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/xhs.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/post.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/types/result.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub/utils/utils.py +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.0.19 → parsehub-2.0.20}/test/test_core_offline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parsehub
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.20
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -37,7 +37,6 @@ Requires-Dist: cryptography>=46.0.6
|
|
|
37
37
|
Requires-Dist: gmssl>=3.2.2
|
|
38
38
|
Provides-Extra: cli
|
|
39
39
|
Requires-Dist: argcomplete>=3.6.3; extra == "cli"
|
|
40
|
-
Requires-Dist: keyring>=25.6.0; extra == "cli"
|
|
41
40
|
Requires-Dist: platformdirs>=4.5.1; extra == "cli"
|
|
42
41
|
Dynamic: license-file
|
|
43
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "parsehub"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.20"
|
|
4
4
|
description = "轻量、异步、开箱即用的社交媒体聚合解析库"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12.0"
|
|
@@ -47,7 +47,6 @@ ph = "parsehub.cli:main"
|
|
|
47
47
|
[project.optional-dependencies]
|
|
48
48
|
cli = [
|
|
49
49
|
"argcomplete>=3.6.3",
|
|
50
|
-
"keyring>=25.6.0",
|
|
51
50
|
"platformdirs>=4.5.1",
|
|
52
51
|
]
|
|
53
52
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import importlib.util
|
|
4
5
|
import json
|
|
5
6
|
import sys
|
|
6
7
|
import unicodedata
|
|
7
8
|
from dataclasses import asdict, is_dataclass
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
from .cli_config import AutoCookieStore,
|
|
13
|
-
from .errors import ParseHubError
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .cli_config import AutoCookieStore, PlatformConfig
|
|
14
14
|
|
|
15
15
|
_COMMANDS = {"parse", "p", "download", "d", "dl", "platforms", "ls", "set"}
|
|
16
|
+
_CLI_EXTRA_MODULES = ("argcomplete", "platformdirs")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class _ChineseHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
@@ -48,15 +49,21 @@ class _ChineseArgumentParser(argparse.ArgumentParser):
|
|
|
48
49
|
|
|
49
50
|
def main(argv: list[str] | None = None) -> int:
|
|
50
51
|
raw_argv = list(sys.argv[1:] if argv is None else argv)
|
|
52
|
+
if not _has_cli_extra_dependencies():
|
|
53
|
+
_print_cli_extra_hint()
|
|
54
|
+
return 1
|
|
51
55
|
parser = _build_parser(Path(sys.argv[0]).name if argv is None else "parsehub")
|
|
52
56
|
_enable_completion(parser)
|
|
57
|
+
if not raw_argv:
|
|
58
|
+
parser.print_help()
|
|
59
|
+
return 0
|
|
53
60
|
try:
|
|
54
61
|
args = parser.parse_args(_normalize_argv(raw_argv))
|
|
55
62
|
_finalize_output_args(args)
|
|
56
63
|
return args.func(args)
|
|
57
64
|
except SystemExit as e:
|
|
58
65
|
return _normalize_exit_code(e.code)
|
|
59
|
-
except
|
|
66
|
+
except ValueError as e:
|
|
60
67
|
_print_error(e)
|
|
61
68
|
return 1
|
|
62
69
|
except KeyboardInterrupt:
|
|
@@ -221,7 +228,7 @@ def _add_json_options(parser: argparse.ArgumentParser) -> None:
|
|
|
221
228
|
|
|
222
229
|
|
|
223
230
|
def _cmd_parse(args: argparse.Namespace) -> int:
|
|
224
|
-
hub =
|
|
231
|
+
hub = _new_parsehub()
|
|
225
232
|
platform_id = _detect_platform_id(hub, args.url_or_text)
|
|
226
233
|
config = _load_platform_config(platform_id)
|
|
227
234
|
proxy = args.proxy if args.proxy is not None else config.parse_proxy
|
|
@@ -235,7 +242,7 @@ def _cmd_parse(args: argparse.Namespace) -> int:
|
|
|
235
242
|
|
|
236
243
|
|
|
237
244
|
def _cmd_download(args: argparse.Namespace) -> int:
|
|
238
|
-
hub =
|
|
245
|
+
hub = _new_parsehub()
|
|
239
246
|
platform_id = _detect_platform_id(hub, args.url_or_text)
|
|
240
247
|
config = _load_platform_config(platform_id)
|
|
241
248
|
proxy = args.proxy if args.proxy is not None else config.download_proxy
|
|
@@ -265,7 +272,7 @@ def _cmd_download(args: argparse.Namespace) -> int:
|
|
|
265
272
|
|
|
266
273
|
|
|
267
274
|
def _cmd_platforms(args: argparse.Namespace) -> int:
|
|
268
|
-
platforms =
|
|
275
|
+
platforms = _new_parsehub().get_platforms()
|
|
269
276
|
if args.json:
|
|
270
277
|
_print_json(platforms, pretty=args.pretty)
|
|
271
278
|
else:
|
|
@@ -284,7 +291,7 @@ def _cmd_platform_list(args: argparse.Namespace) -> int:
|
|
|
284
291
|
|
|
285
292
|
def _cmd_platform_show(args: argparse.Namespace) -> int:
|
|
286
293
|
platform = _validate_platform(args.platform)
|
|
287
|
-
config =
|
|
294
|
+
config = _config_store().get_platform(platform)
|
|
288
295
|
data = _platform_config_row(_platform_info_map().get(platform, {"id": platform, "name": platform}), config)
|
|
289
296
|
if args.json:
|
|
290
297
|
_print_json(data, pretty=args.pretty)
|
|
@@ -298,7 +305,7 @@ def _cmd_platform_proxy(args: argparse.Namespace) -> int:
|
|
|
298
305
|
if args.clear:
|
|
299
306
|
if args.proxy:
|
|
300
307
|
raise ValueError("清除代理时不需要填写代理地址。\n示例: parsehub set proxy xhs --clear")
|
|
301
|
-
changed =
|
|
308
|
+
changed = _config_store().clear_proxy(platform, args.proxy_target)
|
|
302
309
|
if changed:
|
|
303
310
|
print(f"已清除 {platform} 的{_proxy_target_label(args.proxy_target)}。")
|
|
304
311
|
else:
|
|
@@ -306,7 +313,7 @@ def _cmd_platform_proxy(args: argparse.Namespace) -> int:
|
|
|
306
313
|
return 0
|
|
307
314
|
if not args.proxy:
|
|
308
315
|
raise ValueError("缺少代理地址。\n示例: parsehub set proxy xhs http://127.0.0.1:7890")
|
|
309
|
-
|
|
316
|
+
_config_store().set_proxy(platform, args.proxy, args.proxy_target)
|
|
310
317
|
print(f"已设置 {platform} 的{_proxy_target_label(args.proxy_target)}。")
|
|
311
318
|
print(f"代理地址: {args.proxy}")
|
|
312
319
|
return 0
|
|
@@ -314,14 +321,11 @@ def _cmd_platform_proxy(args: argparse.Namespace) -> int:
|
|
|
314
321
|
|
|
315
322
|
def _cmd_platform_cookie(args: argparse.Namespace) -> int:
|
|
316
323
|
platform = _validate_platform(args.platform)
|
|
317
|
-
store =
|
|
324
|
+
store = _cookie_store()
|
|
318
325
|
if args.clear:
|
|
319
326
|
print(f"已清除 {platform} Cookie。" if store.delete(platform) else f"{platform} 还没有保存 Cookie,无需清除。")
|
|
320
327
|
return 0
|
|
321
|
-
|
|
322
|
-
if storage == "file":
|
|
323
|
-
print("提示: 系统密钥库不可用,Cookie 已保存到本地 cookies.toml。", file=sys.stderr)
|
|
324
|
-
print(" 请不要把该文件提交到 git。", file=sys.stderr)
|
|
328
|
+
store.set(platform, _cookie_prompt().read(platform))
|
|
325
329
|
print(f"已保存 {platform} Cookie。之后解析或下载该平台内容时会自动使用。")
|
|
326
330
|
return 0
|
|
327
331
|
|
|
@@ -333,16 +337,46 @@ def _print_error(error: Exception) -> None:
|
|
|
333
337
|
print(f" {line}", file=sys.stderr)
|
|
334
338
|
|
|
335
339
|
|
|
340
|
+
def _new_parsehub() -> Any:
|
|
341
|
+
from . import ParseHub
|
|
342
|
+
|
|
343
|
+
return ParseHub()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _platform_config_type() -> type:
|
|
347
|
+
from .cli_config import PlatformConfig
|
|
348
|
+
|
|
349
|
+
return PlatformConfig
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _config_store() -> Any:
|
|
353
|
+
from .cli_config import ConfigStore
|
|
354
|
+
|
|
355
|
+
return ConfigStore()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cookie_store() -> Any:
|
|
359
|
+
from .cli_config import AutoCookieStore
|
|
360
|
+
|
|
361
|
+
return AutoCookieStore()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _cookie_prompt() -> Any:
|
|
365
|
+
from .cli_config import CookiePrompt
|
|
366
|
+
|
|
367
|
+
return CookiePrompt()
|
|
368
|
+
|
|
369
|
+
|
|
336
370
|
def _load_platform_config(platform_id: str | None) -> PlatformConfig:
|
|
337
371
|
if not platform_id:
|
|
338
|
-
return
|
|
339
|
-
return
|
|
372
|
+
return _platform_config_type()()
|
|
373
|
+
return _config_store().get_platform(platform_id)
|
|
340
374
|
|
|
341
375
|
|
|
342
376
|
def _load_cookie(platform_id: str | None) -> str | None:
|
|
343
377
|
if not platform_id:
|
|
344
378
|
return None
|
|
345
|
-
return
|
|
379
|
+
return _cookie_store().get(platform_id)
|
|
346
380
|
|
|
347
381
|
|
|
348
382
|
def _detect_platform_id(hub: Any, url_or_text: str) -> str | None:
|
|
@@ -372,19 +406,19 @@ def _validate_platform(platform: str) -> str:
|
|
|
372
406
|
|
|
373
407
|
|
|
374
408
|
def _supported_platform_ids() -> list[str]:
|
|
375
|
-
return [str(platform.get("id")) for platform in
|
|
409
|
+
return [str(platform.get("id")) for platform in _new_parsehub().get_platforms() if platform.get("id")]
|
|
376
410
|
|
|
377
411
|
|
|
378
412
|
def _platform_info_map() -> dict[str, dict[str, Any]]:
|
|
379
|
-
return {str(platform.get("id")): platform for platform in
|
|
413
|
+
return {str(platform.get("id")): platform for platform in _new_parsehub().get_platforms() if platform.get("id")}
|
|
380
414
|
|
|
381
415
|
|
|
382
416
|
def _platform_config_rows() -> list[dict[str, Any]]:
|
|
383
|
-
config_store =
|
|
384
|
-
cookie_store =
|
|
417
|
+
config_store = _config_store()
|
|
418
|
+
cookie_store = _cookie_store()
|
|
385
419
|
return [
|
|
386
420
|
_platform_config_row(platform, config_store.get_platform(str(platform["id"])), cookie_store=cookie_store)
|
|
387
|
-
for platform in
|
|
421
|
+
for platform in _new_parsehub().get_platforms()
|
|
388
422
|
]
|
|
389
423
|
|
|
390
424
|
|
|
@@ -396,7 +430,7 @@ def _platform_config_row(
|
|
|
396
430
|
) -> dict[str, Any]:
|
|
397
431
|
platform_id = str(platform.get("id") or "")
|
|
398
432
|
if cookie_store is None:
|
|
399
|
-
cookie_store =
|
|
433
|
+
cookie_store = _cookie_store()
|
|
400
434
|
return {
|
|
401
435
|
"id": platform_id,
|
|
402
436
|
"name": str(platform.get("name") or ""),
|
|
@@ -595,11 +629,19 @@ def _complete_platforms(prefix: str, **_: Any) -> list[str]:
|
|
|
595
629
|
return [platform for platform in _supported_platform_ids() if platform.startswith(prefix)]
|
|
596
630
|
|
|
597
631
|
|
|
632
|
+
def _has_cli_extra_dependencies() -> bool:
|
|
633
|
+
return all(importlib.util.find_spec(module) is not None for module in _CLI_EXTRA_MODULES)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _print_cli_extra_hint() -> None:
|
|
637
|
+
print("错误: 未安装 ParseHub CLI 扩展依赖。", file=sys.stderr)
|
|
638
|
+
print('请运行: pip install "parsehub[cli]"', file=sys.stderr)
|
|
639
|
+
print('如果使用 uv: uv add "parsehub[cli]"', file=sys.stderr)
|
|
640
|
+
|
|
641
|
+
|
|
598
642
|
def _enable_completion(parser: argparse.ArgumentParser) -> None:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
except Exception:
|
|
602
|
-
return
|
|
643
|
+
import argcomplete
|
|
644
|
+
|
|
603
645
|
argcomplete.autocomplete(parser)
|
|
604
646
|
|
|
605
647
|
|
|
@@ -79,47 +79,6 @@ class ConfigStore:
|
|
|
79
79
|
return changed
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
class KeyringUnavailable(RuntimeError):
|
|
83
|
-
pass
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class KeyringCookieStore:
|
|
87
|
-
def __init__(self, service: str = "parsehub"):
|
|
88
|
-
self.service = service
|
|
89
|
-
|
|
90
|
-
def set(self, platform: str, cookie: str) -> None:
|
|
91
|
-
keyring = self._keyring()
|
|
92
|
-
try:
|
|
93
|
-
keyring.set_password(self.service, self._username(platform), cookie)
|
|
94
|
-
except Exception as e:
|
|
95
|
-
raise KeyringUnavailable(str(e)) from e
|
|
96
|
-
|
|
97
|
-
def get(self, platform: str) -> str | None:
|
|
98
|
-
keyring = self._keyring()
|
|
99
|
-
try:
|
|
100
|
-
return keyring.get_password(self.service, self._username(platform))
|
|
101
|
-
except Exception as e:
|
|
102
|
-
raise KeyringUnavailable(str(e)) from e
|
|
103
|
-
|
|
104
|
-
def delete(self, platform: str) -> bool:
|
|
105
|
-
try:
|
|
106
|
-
keyring = self._keyring()
|
|
107
|
-
keyring.delete_password(self.service, self._username(platform))
|
|
108
|
-
return True
|
|
109
|
-
except Exception:
|
|
110
|
-
return False
|
|
111
|
-
|
|
112
|
-
def _keyring(self) -> Any:
|
|
113
|
-
try:
|
|
114
|
-
import keyring
|
|
115
|
-
except Exception as e:
|
|
116
|
-
raise KeyringUnavailable("keyring 未安装") from e
|
|
117
|
-
return keyring
|
|
118
|
-
|
|
119
|
-
def _username(self, platform: str) -> str:
|
|
120
|
-
return f"cookie:{platform}"
|
|
121
|
-
|
|
122
|
-
|
|
123
82
|
class FileCookieStore:
|
|
124
83
|
def __init__(self, path: Path | None = None):
|
|
125
84
|
self.path = default_cookie_path() if path is None else Path(path)
|
|
@@ -163,36 +122,20 @@ class FileCookieStore:
|
|
|
163
122
|
|
|
164
123
|
|
|
165
124
|
class AutoCookieStore:
|
|
166
|
-
def __init__(
|
|
167
|
-
self,
|
|
168
|
-
keyring_store: KeyringCookieStore | None = None,
|
|
169
|
-
file_store: FileCookieStore | None = None,
|
|
170
|
-
):
|
|
171
|
-
self.keyring_store = KeyringCookieStore() if keyring_store is None else keyring_store
|
|
125
|
+
def __init__(self, file_store: FileCookieStore | None = None):
|
|
172
126
|
self.file_store = FileCookieStore() if file_store is None else file_store
|
|
173
127
|
|
|
174
|
-
def set(self, platform: str, cookie: str) ->
|
|
175
|
-
|
|
176
|
-
self.keyring_store.set(platform, cookie)
|
|
177
|
-
self.file_store.delete(platform)
|
|
178
|
-
return "keyring"
|
|
179
|
-
except KeyringUnavailable:
|
|
180
|
-
self.file_store.set(platform, cookie)
|
|
181
|
-
return "file"
|
|
128
|
+
def set(self, platform: str, cookie: str) -> None:
|
|
129
|
+
self.file_store.set(platform, cookie)
|
|
182
130
|
|
|
183
131
|
def get(self, platform: str) -> str | None:
|
|
184
|
-
|
|
185
|
-
value = self.keyring_store.get(platform)
|
|
186
|
-
except KeyringUnavailable:
|
|
187
|
-
value = None
|
|
188
|
-
return value or self.file_store.get(platform)
|
|
132
|
+
return self.file_store.get(platform)
|
|
189
133
|
|
|
190
134
|
def delete(self, platform: str) -> bool:
|
|
191
|
-
|
|
192
|
-
return self.file_store.delete(platform) or deleted
|
|
135
|
+
return self.file_store.delete(platform)
|
|
193
136
|
|
|
194
137
|
def exists(self, platform: str) -> bool:
|
|
195
|
-
return self.
|
|
138
|
+
return self.file_store.exists(platform)
|
|
196
139
|
|
|
197
140
|
|
|
198
141
|
class CookiePrompt:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parsehub
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.20
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -37,7 +37,6 @@ Requires-Dist: cryptography>=46.0.6
|
|
|
37
37
|
Requires-Dist: gmssl>=3.2.2
|
|
38
38
|
Provides-Extra: cli
|
|
39
39
|
Requires-Dist: argcomplete>=3.6.3; extra == "cli"
|
|
40
|
-
Requires-Dist: keyring>=25.6.0; extra == "cli"
|
|
41
40
|
Requires-Dist: platformdirs>=4.5.1; extra == "cli"
|
|
42
41
|
Dynamic: license-file
|
|
43
42
|
|
|
@@ -5,7 +5,7 @@ import tempfile
|
|
|
5
5
|
import unittest
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from unittest.mock import patch
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
9
|
|
|
10
10
|
from src.parsehub import cli
|
|
11
11
|
from src.parsehub.cli_config import ConfigStore, FileCookieStore
|
|
@@ -118,8 +118,8 @@ class TestCli(unittest.TestCase):
|
|
|
118
118
|
self.config_path = self.config_dir / "config.toml"
|
|
119
119
|
self.cookie_path = self.config_dir / "cookies.toml"
|
|
120
120
|
self.patches = [
|
|
121
|
-
patch.object(cli, "
|
|
122
|
-
patch.object(cli, "
|
|
121
|
+
patch.object(cli, "_config_store", lambda: ConfigStore(self.config_path)),
|
|
122
|
+
patch.object(cli, "_cookie_store", lambda: FileCookieStore(self.cookie_path)),
|
|
123
123
|
]
|
|
124
124
|
for item in self.patches:
|
|
125
125
|
item.start()
|
|
@@ -128,12 +128,42 @@ class TestCli(unittest.TestCase):
|
|
|
128
128
|
def run_cli(self, argv):
|
|
129
129
|
stdout = io.StringIO()
|
|
130
130
|
stderr = io.StringIO()
|
|
131
|
-
with
|
|
131
|
+
with (
|
|
132
|
+
patch.object(cli, "_has_cli_extra_dependencies", return_value=True),
|
|
133
|
+
patch.object(cli, "_enable_completion", return_value=None),
|
|
134
|
+
contextlib.redirect_stdout(stdout),
|
|
135
|
+
contextlib.redirect_stderr(stderr),
|
|
136
|
+
):
|
|
132
137
|
code = cli.main(argv)
|
|
133
138
|
return code, stdout.getvalue(), stderr.getvalue()
|
|
134
139
|
|
|
140
|
+
def test_missing_cli_extra_prints_install_hint(self):
|
|
141
|
+
stdout = io.StringIO()
|
|
142
|
+
stderr = io.StringIO()
|
|
143
|
+
with (
|
|
144
|
+
patch.object(cli, "_has_cli_extra_dependencies", return_value=False),
|
|
145
|
+
patch.object(cli, "_build_parser") as build_parser,
|
|
146
|
+
contextlib.redirect_stdout(stdout),
|
|
147
|
+
contextlib.redirect_stderr(stderr),
|
|
148
|
+
):
|
|
149
|
+
code = cli.main(["platforms"])
|
|
150
|
+
|
|
151
|
+
self.assertEqual(code, 1)
|
|
152
|
+
self.assertEqual(stdout.getvalue(), "")
|
|
153
|
+
self.assertIn("未安装 ParseHub CLI 扩展依赖", stderr.getvalue())
|
|
154
|
+
self.assertIn('pip install "parsehub[cli]"', stderr.getvalue())
|
|
155
|
+
build_parser.assert_not_called()
|
|
156
|
+
|
|
157
|
+
def test_empty_args_print_help(self):
|
|
158
|
+
code, stdout, stderr = self.run_cli([])
|
|
159
|
+
|
|
160
|
+
self.assertEqual(code, 0)
|
|
161
|
+
self.assertEqual(stderr, "")
|
|
162
|
+
self.assertIn("ParseHub 命令行工具", stdout)
|
|
163
|
+
self.assertIn("用法:", stdout)
|
|
164
|
+
|
|
135
165
|
def test_parse_defaults_to_human_readable_chinese_summary(self):
|
|
136
|
-
with patch.object(cli, "
|
|
166
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
137
167
|
code, stdout, stderr = self.run_cli(
|
|
138
168
|
["parse", "分享 https://example.com/post/1", "--proxy", "http://proxy", "--cookie", "a=b"]
|
|
139
169
|
)
|
|
@@ -148,7 +178,7 @@ class TestCli(unittest.TestCase):
|
|
|
148
178
|
)
|
|
149
179
|
|
|
150
180
|
def test_parse_json_outputs_json(self):
|
|
151
|
-
with patch.object(cli, "
|
|
181
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
152
182
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1", "--json"])
|
|
153
183
|
|
|
154
184
|
self.assertEqual(code, 0)
|
|
@@ -156,7 +186,7 @@ class TestCli(unittest.TestCase):
|
|
|
156
186
|
self.assertEqual(json.loads(stdout)["title"], "标题")
|
|
157
187
|
|
|
158
188
|
def test_parse_compact_outputs_single_line_json(self):
|
|
159
|
-
with patch.object(cli, "
|
|
189
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
160
190
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1", "--compact"])
|
|
161
191
|
|
|
162
192
|
self.assertEqual(code, 0)
|
|
@@ -165,7 +195,7 @@ class TestCli(unittest.TestCase):
|
|
|
165
195
|
self.assertEqual(json.loads(stdout)["platform"], "xhs")
|
|
166
196
|
|
|
167
197
|
def test_short_parse_alias_and_default_parse(self):
|
|
168
|
-
with patch.object(cli, "
|
|
198
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
169
199
|
alias_code, alias_stdout, alias_stderr = self.run_cli(["p", "https://example.com/post/1"])
|
|
170
200
|
default_code, default_stdout, default_stderr = self.run_cli(["https://example.com/post/2"])
|
|
171
201
|
|
|
@@ -179,7 +209,7 @@ class TestCli(unittest.TestCase):
|
|
|
179
209
|
self.assertEqual(FakeParseHub.instances[1].parse_calls[0]["url"], "https://example.com/post/2")
|
|
180
210
|
|
|
181
211
|
def test_download_outputs_summary_progress_and_forwards_options(self):
|
|
182
|
-
with patch.object(cli, "
|
|
212
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
183
213
|
code, stdout, stderr = self.run_cli(
|
|
184
214
|
[
|
|
185
215
|
"download",
|
|
@@ -211,7 +241,7 @@ class TestCli(unittest.TestCase):
|
|
|
211
241
|
self.assertTrue(call["save_metadata"])
|
|
212
242
|
|
|
213
243
|
def test_short_download_alias_outputs_json_and_forwards_output_dir(self):
|
|
214
|
-
with patch.object(cli, "
|
|
244
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
215
245
|
code, stdout, stderr = self.run_cli(["d", "https://example.com/post/1", "--output-dir", "./out", "--json"])
|
|
216
246
|
|
|
217
247
|
self.assertEqual(code, 0)
|
|
@@ -222,7 +252,7 @@ class TestCli(unittest.TestCase):
|
|
|
222
252
|
self.assertEqual(FakeParseHub.instances[0].download_calls[0]["path"], "./out")
|
|
223
253
|
|
|
224
254
|
def test_download_quiet_suppresses_feedback_and_progress_callback(self):
|
|
225
|
-
with patch.object(cli, "
|
|
255
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
226
256
|
code, stdout, stderr = self.run_cli(["dl", "https://example.com/post/1", "--quiet"])
|
|
227
257
|
|
|
228
258
|
self.assertEqual(code, 0)
|
|
@@ -231,7 +261,7 @@ class TestCli(unittest.TestCase):
|
|
|
231
261
|
self.assertIsNone(FakeParseHub.instances[0].download_calls[0]["callback"])
|
|
232
262
|
|
|
233
263
|
def test_download_no_progress_keeps_status_but_disables_callback(self):
|
|
234
|
-
with patch.object(cli, "
|
|
264
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
235
265
|
code, stdout, stderr = self.run_cli(["download", "https://example.com/post/1", "--no-progress"])
|
|
236
266
|
|
|
237
267
|
self.assertEqual(code, 0)
|
|
@@ -241,7 +271,7 @@ class TestCli(unittest.TestCase):
|
|
|
241
271
|
self.assertIsNone(FakeParseHub.instances[0].download_calls[0]["callback"])
|
|
242
272
|
|
|
243
273
|
def test_download_defaults_path_to_cwd_downloads(self):
|
|
244
|
-
with patch.object(cli, "
|
|
274
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
245
275
|
code, stdout, stderr = self.run_cli(["download", "https://example.com/post/1", "--quiet"])
|
|
246
276
|
|
|
247
277
|
self.assertEqual(code, 0)
|
|
@@ -249,7 +279,7 @@ class TestCli(unittest.TestCase):
|
|
|
249
279
|
self.assertEqual(FakeParseHub.instances[0].download_calls[0]["path"], Path.cwd() / "downloads")
|
|
250
280
|
|
|
251
281
|
def test_platforms_outputs_aligned_human_readable_table(self):
|
|
252
|
-
with patch.object(cli, "
|
|
282
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
253
283
|
code, stdout, stderr = self.run_cli(["platforms"])
|
|
254
284
|
|
|
255
285
|
self.assertEqual(code, 0)
|
|
@@ -261,7 +291,7 @@ class TestCli(unittest.TestCase):
|
|
|
261
291
|
self.assertEqual(lines[3], "weibo 微博 视频")
|
|
262
292
|
|
|
263
293
|
def test_short_platforms_alias_outputs_json(self):
|
|
264
|
-
with patch.object(cli, "
|
|
294
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
265
295
|
code, stdout, stderr = self.run_cli(["ls", "--json"])
|
|
266
296
|
|
|
267
297
|
self.assertEqual(code, 0)
|
|
@@ -275,7 +305,7 @@ class TestCli(unittest.TestCase):
|
|
|
275
305
|
)
|
|
276
306
|
|
|
277
307
|
def test_set_proxy_sets_and_shows_parse_and_download_proxy(self):
|
|
278
|
-
with patch.object(cli, "
|
|
308
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
279
309
|
set_code, set_stdout, set_stderr = self.run_cli(["set", "proxy", "xhs", "http://proxy"])
|
|
280
310
|
show_code, show_stdout, show_stderr = self.run_cli(["set", "show", "xhs"])
|
|
281
311
|
|
|
@@ -291,7 +321,7 @@ class TestCli(unittest.TestCase):
|
|
|
291
321
|
self.assertIn('download_proxy = "http://proxy"', self.config_path.read_text())
|
|
292
322
|
|
|
293
323
|
def test_set_proxy_supports_targeted_clear(self):
|
|
294
|
-
with patch.object(cli, "
|
|
324
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
295
325
|
self.run_cli(["set", "proxy", "xhs", "http://parse", "--for", "parse"])
|
|
296
326
|
self.run_cli(["set", "proxy", "xhs", "http://download", "--for", "download"])
|
|
297
327
|
code, stdout, stderr = self.run_cli(["set", "proxy", "xhs", "--clear", "--for", "parse"])
|
|
@@ -306,9 +336,11 @@ class TestCli(unittest.TestCase):
|
|
|
306
336
|
self.assertIn("下载代理: http://download", show_stdout)
|
|
307
337
|
|
|
308
338
|
def test_set_cookie_sets_lists_and_clears_cookie_without_printing_value(self):
|
|
339
|
+
prompt = Mock()
|
|
340
|
+
prompt.read.return_value = "a=b; token=secret"
|
|
309
341
|
with (
|
|
310
|
-
patch.object(cli, "
|
|
311
|
-
patch.object(cli
|
|
342
|
+
patch.object(cli, "_new_parsehub", FakeParseHub),
|
|
343
|
+
patch.object(cli, "_cookie_prompt", return_value=prompt),
|
|
312
344
|
):
|
|
313
345
|
set_code, set_stdout, set_stderr = self.run_cli(["set", "cookie", "xhs"])
|
|
314
346
|
list_code, list_stdout, list_stderr = self.run_cli(["set", "list"])
|
|
@@ -330,7 +362,7 @@ class TestCli(unittest.TestCase):
|
|
|
330
362
|
def test_parse_uses_saved_platform_proxy_and_cookie(self):
|
|
331
363
|
ConfigStore(self.config_path).set_proxy("xhs", "http://parse-proxy", "parse")
|
|
332
364
|
FileCookieStore(self.cookie_path).set("xhs", "saved=cookie")
|
|
333
|
-
with patch.object(cli, "
|
|
365
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
334
366
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1"])
|
|
335
367
|
|
|
336
368
|
self.assertEqual(code, 0)
|
|
@@ -344,7 +376,7 @@ class TestCli(unittest.TestCase):
|
|
|
344
376
|
store.set_proxy("xhs", "http://parse-proxy", "parse")
|
|
345
377
|
store.set_proxy("xhs", "http://download-proxy", "download")
|
|
346
378
|
FileCookieStore(self.cookie_path).set("xhs", "saved=cookie")
|
|
347
|
-
with patch.object(cli, "
|
|
379
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
348
380
|
code, stdout, stderr = self.run_cli(["download", "https://example.com/post/1", "--quiet"])
|
|
349
381
|
|
|
350
382
|
self.assertEqual(code, 0)
|
|
@@ -358,7 +390,7 @@ class TestCli(unittest.TestCase):
|
|
|
358
390
|
def test_cli_options_override_saved_platform_config(self):
|
|
359
391
|
ConfigStore(self.config_path).set_proxy("xhs", "http://saved-proxy", "all")
|
|
360
392
|
FileCookieStore(self.cookie_path).set("xhs", "saved=cookie")
|
|
361
|
-
with patch.object(cli, "
|
|
393
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
362
394
|
code, stdout, stderr = self.run_cli(
|
|
363
395
|
[
|
|
364
396
|
"download",
|
|
@@ -382,7 +414,7 @@ class TestCli(unittest.TestCase):
|
|
|
382
414
|
self.assertEqual(call["parse_cookie"], "cli=cookie")
|
|
383
415
|
|
|
384
416
|
def test_parsehub_error_returns_one(self):
|
|
385
|
-
with patch.object(cli, "
|
|
417
|
+
with patch.object(cli, "_new_parsehub", ErrorParseHub):
|
|
386
418
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1"])
|
|
387
419
|
|
|
388
420
|
self.assertEqual(code, 1)
|
|
@@ -391,7 +423,7 @@ class TestCli(unittest.TestCase):
|
|
|
391
423
|
self.assertIn("错误", stderr)
|
|
392
424
|
|
|
393
425
|
def test_value_error_returns_one(self):
|
|
394
|
-
with patch.object(cli, "
|
|
426
|
+
with patch.object(cli, "_new_parsehub", ValueErrorParseHub):
|
|
395
427
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1"])
|
|
396
428
|
|
|
397
429
|
self.assertEqual(code, 1)
|
|
@@ -400,7 +432,7 @@ class TestCli(unittest.TestCase):
|
|
|
400
432
|
self.assertIn("错误", stderr)
|
|
401
433
|
|
|
402
434
|
def test_keyboard_interrupt_returns_130(self):
|
|
403
|
-
with patch.object(cli, "
|
|
435
|
+
with patch.object(cli, "_new_parsehub", KeyboardInterruptParseHub):
|
|
404
436
|
code, stdout, stderr = self.run_cli(["parse", "https://example.com/post/1"])
|
|
405
437
|
|
|
406
438
|
self.assertEqual(code, 130)
|
|
@@ -426,7 +458,7 @@ class TestCli(unittest.TestCase):
|
|
|
426
458
|
self.assertIn("parsehub set proxy xhs", stdout)
|
|
427
459
|
|
|
428
460
|
def test_set_proxy_missing_value_shows_actionable_example(self):
|
|
429
|
-
with patch.object(cli, "
|
|
461
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
430
462
|
code, stdout, stderr = self.run_cli(["set", "proxy", "xhs"])
|
|
431
463
|
|
|
432
464
|
self.assertEqual(code, 1)
|
|
@@ -436,7 +468,7 @@ class TestCli(unittest.TestCase):
|
|
|
436
468
|
self.assertIn("\n 示例:", stderr)
|
|
437
469
|
|
|
438
470
|
def test_unknown_platform_error_lists_next_step(self):
|
|
439
|
-
with patch.object(cli, "
|
|
471
|
+
with patch.object(cli, "_new_parsehub", FakeParseHub):
|
|
440
472
|
code, stdout, stderr = self.run_cli(["set", "show", "unknown"])
|
|
441
473
|
|
|
442
474
|
self.assertEqual(code, 1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from src.parsehub.cli_config import AutoCookieStore, ConfigStore, FileCookieStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestCliConfig(unittest.TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
11
|
+
self.addCleanup(self.tmp.cleanup)
|
|
12
|
+
self.base = Path(self.tmp.name)
|
|
13
|
+
|
|
14
|
+
def test_config_store_sets_and_clears_targeted_proxy(self):
|
|
15
|
+
store = ConfigStore(self.base / "config.toml")
|
|
16
|
+
|
|
17
|
+
store.set_proxy("xhs", "http://parse", "parse")
|
|
18
|
+
store.set_proxy("xhs", "http://download", "download")
|
|
19
|
+
|
|
20
|
+
config = store.get_platform("xhs")
|
|
21
|
+
self.assertEqual(config.parse_proxy, "http://parse")
|
|
22
|
+
self.assertEqual(config.download_proxy, "http://download")
|
|
23
|
+
|
|
24
|
+
self.assertTrue(store.clear_proxy("xhs", "parse"))
|
|
25
|
+
config = store.get_platform("xhs")
|
|
26
|
+
self.assertIsNone(config.parse_proxy)
|
|
27
|
+
self.assertEqual(config.download_proxy, "http://download")
|
|
28
|
+
|
|
29
|
+
def test_auto_cookie_store_uses_private_file(self):
|
|
30
|
+
path = self.base / "cookies.toml"
|
|
31
|
+
store = AutoCookieStore(file_store=FileCookieStore(path))
|
|
32
|
+
|
|
33
|
+
store.set("xhs", "a=b")
|
|
34
|
+
|
|
35
|
+
self.assertEqual(store.get("xhs"), "a=b")
|
|
36
|
+
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
|
37
|
+
|
|
38
|
+
def test_auto_cookie_store_deletes_private_file_cookie(self):
|
|
39
|
+
path = self.base / "cookies.toml"
|
|
40
|
+
store = AutoCookieStore(file_store=FileCookieStore(path))
|
|
41
|
+
store.set("xhs", "a=b")
|
|
42
|
+
|
|
43
|
+
self.assertTrue(store.delete("xhs"))
|
|
44
|
+
|
|
45
|
+
self.assertFalse(store.exists("xhs"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
import unittest
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from src.parsehub.cli_config import AutoCookieStore, ConfigStore, FileCookieStore, KeyringUnavailable
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class UnavailableKeyringStore:
|
|
9
|
-
def set(self, platform, cookie):
|
|
10
|
-
raise KeyringUnavailable("unavailable")
|
|
11
|
-
|
|
12
|
-
def get(self, platform):
|
|
13
|
-
raise KeyringUnavailable("unavailable")
|
|
14
|
-
|
|
15
|
-
def delete(self, platform):
|
|
16
|
-
return False
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class MemoryKeyringStore:
|
|
20
|
-
def __init__(self):
|
|
21
|
-
self.values = {}
|
|
22
|
-
|
|
23
|
-
def set(self, platform, cookie):
|
|
24
|
-
self.values[platform] = cookie
|
|
25
|
-
|
|
26
|
-
def get(self, platform):
|
|
27
|
-
return self.values.get(platform)
|
|
28
|
-
|
|
29
|
-
def delete(self, platform):
|
|
30
|
-
return self.values.pop(platform, None) is not None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class TestCliConfig(unittest.TestCase):
|
|
34
|
-
def setUp(self):
|
|
35
|
-
self.tmp = tempfile.TemporaryDirectory()
|
|
36
|
-
self.addCleanup(self.tmp.cleanup)
|
|
37
|
-
self.base = Path(self.tmp.name)
|
|
38
|
-
|
|
39
|
-
def test_config_store_sets_and_clears_targeted_proxy(self):
|
|
40
|
-
store = ConfigStore(self.base / "config.toml")
|
|
41
|
-
|
|
42
|
-
store.set_proxy("xhs", "http://parse", "parse")
|
|
43
|
-
store.set_proxy("xhs", "http://download", "download")
|
|
44
|
-
|
|
45
|
-
config = store.get_platform("xhs")
|
|
46
|
-
self.assertEqual(config.parse_proxy, "http://parse")
|
|
47
|
-
self.assertEqual(config.download_proxy, "http://download")
|
|
48
|
-
|
|
49
|
-
self.assertTrue(store.clear_proxy("xhs", "parse"))
|
|
50
|
-
config = store.get_platform("xhs")
|
|
51
|
-
self.assertIsNone(config.parse_proxy)
|
|
52
|
-
self.assertEqual(config.download_proxy, "http://download")
|
|
53
|
-
|
|
54
|
-
def test_auto_cookie_store_falls_back_to_private_file(self):
|
|
55
|
-
path = self.base / "cookies.toml"
|
|
56
|
-
store = AutoCookieStore(keyring_store=UnavailableKeyringStore(), file_store=FileCookieStore(path))
|
|
57
|
-
|
|
58
|
-
storage = store.set("xhs", "a=b")
|
|
59
|
-
|
|
60
|
-
self.assertEqual(storage, "file")
|
|
61
|
-
self.assertEqual(store.get("xhs"), "a=b")
|
|
62
|
-
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
|
63
|
-
|
|
64
|
-
def test_auto_cookie_store_prefers_keyring_and_removes_file_fallback(self):
|
|
65
|
-
path = self.base / "cookies.toml"
|
|
66
|
-
file_store = FileCookieStore(path)
|
|
67
|
-
file_store.set("xhs", "old=file")
|
|
68
|
-
keyring_store = MemoryKeyringStore()
|
|
69
|
-
store = AutoCookieStore(keyring_store=keyring_store, file_store=file_store)
|
|
70
|
-
|
|
71
|
-
storage = store.set("xhs", "new=keyring")
|
|
72
|
-
|
|
73
|
-
self.assertEqual(storage, "keyring")
|
|
74
|
-
self.assertEqual(store.get("xhs"), "new=keyring")
|
|
75
|
-
self.assertFalse(file_store.exists("xhs"))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if __name__ == "__main__":
|
|
79
|
-
unittest.main()
|
|
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
|
|
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
|
|
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
|