feedscope 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.
Files changed (72) hide show
  1. feedscope/__init__.py +3 -0
  2. feedscope/__main__.py +4 -0
  3. feedscope/cli.py +154 -0
  4. feedscope/core/__init__.py +1 -0
  5. feedscope/core/browser.py +367 -0
  6. feedscope/core/config.py +367 -0
  7. feedscope/core/const.py +64 -0
  8. feedscope/core/display.py +62 -0
  9. feedscope/core/http.py +104 -0
  10. feedscope/core/i18n/__init__.py +202 -0
  11. feedscope/core/i18n/en.toml +447 -0
  12. feedscope/core/i18n/zh-CN.toml +444 -0
  13. feedscope/core/media.py +150 -0
  14. feedscope/core/models.py +86 -0
  15. feedscope/core/registry.py +53 -0
  16. feedscope/core/samples.py +68 -0
  17. feedscope/core/source.py +668 -0
  18. feedscope/core/text.py +54 -0
  19. feedscope/core/timefmt.py +53 -0
  20. feedscope/core/tree.py +134 -0
  21. feedscope/doctor.py +54 -0
  22. feedscope/login.py +168 -0
  23. feedscope/probe.py +131 -0
  24. feedscope/report.py +19 -0
  25. feedscope/sources/__init__.py +38 -0
  26. feedscope/sources/hn/__init__.py +66 -0
  27. feedscope/sources/hn/fetch.py +82 -0
  28. feedscope/sources/hn/parse.py +154 -0
  29. feedscope/sources/reddit/__init__.py +178 -0
  30. feedscope/sources/reddit/fetch.py +181 -0
  31. feedscope/sources/reddit/parse.py +318 -0
  32. feedscope/sources/rss/__init__.py +64 -0
  33. feedscope/sources/rss/fetch.py +94 -0
  34. feedscope/sources/rss/parse.py +116 -0
  35. feedscope/sources/telegram/__init__.py +61 -0
  36. feedscope/sources/telegram/fetch.py +79 -0
  37. feedscope/sources/telegram/parse.py +182 -0
  38. feedscope/sources/v2ex/__init__.py +64 -0
  39. feedscope/sources/v2ex/fetch.py +46 -0
  40. feedscope/sources/v2ex/parse.py +116 -0
  41. feedscope/sources/x/__init__.py +68 -0
  42. feedscope/sources/x/fetch.py +490 -0
  43. feedscope/sources/x/parse.py +535 -0
  44. feedscope/sources/zhihu/__init__.py +81 -0
  45. feedscope/sources/zhihu/fetch.py +402 -0
  46. feedscope/sources/zhihu/parse.py +235 -0
  47. feedscope/subscriptions.py +423 -0
  48. feedscope/ui/__init__.py +10 -0
  49. feedscope/ui/account.py +125 -0
  50. feedscope/ui/app.py +105 -0
  51. feedscope/ui/dialogs.py +127 -0
  52. feedscope/ui/feedback.py +64 -0
  53. feedscope/ui/focus_nav.py +113 -0
  54. feedscope/ui/labels.py +38 -0
  55. feedscope/ui/layout.py +66 -0
  56. feedscope/ui/list_render.py +42 -0
  57. feedscope/ui/screens/__init__.py +1 -0
  58. feedscope/ui/screens/answer.py +296 -0
  59. feedscope/ui/screens/detail.py +274 -0
  60. feedscope/ui/screens/language.py +59 -0
  61. feedscope/ui/screens/main.py +736 -0
  62. feedscope/ui/screens/main_setup.py +226 -0
  63. feedscope/ui/screens/reading.py +384 -0
  64. feedscope/ui/screens/source_setup.py +217 -0
  65. feedscope/ui/screens/subscriptions.py +196 -0
  66. feedscope/ui/theme.py +45 -0
  67. feedscope/ui/wrap.py +157 -0
  68. feedscope-0.1.0.dist-info/METADATA +302 -0
  69. feedscope-0.1.0.dist-info/RECORD +72 -0
  70. feedscope-0.1.0.dist-info/WHEEL +4 -0
  71. feedscope-0.1.0.dist-info/entry_points.txt +2 -0
  72. feedscope-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,53 @@
1
+ """源注册器:@register + sources 包导入 = 插件机制。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from feedscope.core.source import Source
6
+
7
+ _REGISTRY: dict[str, type[Source]] = {}
8
+
9
+
10
+ def register(cls: type[Source]) -> type[Source]:
11
+ """类装饰器:把 Source 子类登记进全局注册表。"""
12
+ _REGISTRY[cls.source_id] = cls
13
+ return cls
14
+
15
+
16
+ def source_classes() -> list[type[Source]]:
17
+ """全部已注册的源类(顺序即注册顺序)。
18
+
19
+ 供只需要类的路径(启动时「有没有注册任何源」检查)——要看运行时
20
+ 实例(读配置过滤后的源)走 all_sources。
21
+ """
22
+ return list(_REGISTRY.values())
23
+
24
+
25
+ def all_sources() -> list[Source]:
26
+ """实例化全部已注册源(顺序即注册顺序)。"""
27
+ return [cls() for cls in _REGISTRY.values()]
28
+
29
+
30
+ def get_class(source_id: str) -> type[Source] | None:
31
+ """按 id 取源类(不实例化);未注册返回 None。
32
+
33
+ 供按声明分派的调用方(UI 的源重建、订阅入口的保留字检查)——它们只要
34
+ ClassVar,实例化反而会读配置做合成。取不到直接炸的路径用 get_source。
35
+ """
36
+ return _REGISTRY.get(source_id)
37
+
38
+
39
+ def get_source(source_id: str) -> Source:
40
+ """按 id 实例化源;未注册直接 KeyError(登录/巡检等内部路径 fail-fast)。"""
41
+ return _REGISTRY[source_id]()
42
+
43
+
44
+ def install_sources(classes: list[type[Source]]) -> None:
45
+ """整体替换注册表为给定源类(离线脚本 / 截图驱动 stub 源用)。
46
+
47
+ 测试与 scripts/screenshot.py 之前直接改 _REGISTRY 私有字典——那是对
48
+ 实现细节的耦合(换数据结构即断);公开入口把「替换 + 恢复」的语义
49
+ 收进本模块。
50
+ """
51
+ _REGISTRY.clear()
52
+ for cls in classes:
53
+ register(cls)
@@ -0,0 +1,68 @@
1
+ """样本落盘:把取到的原始响应写到磁盘,站点改版后修 parse 用。
2
+
3
+ - 开关:FEEDSCOPE_SAMPLE_DIR 环境变量(未设 = 关,零开销);feedscope probe 自动设到
4
+ ~/.cache/feedscope/samples/,平时 TUI 里完全不落盘
5
+ - 文件名 = 源标签--URL 路径清洗版(rand 等随机参数剔除、其余参数排序,
6
+ 重跑同名覆盖,样本目录始终是最新的)
7
+ - 一切落盘失败吞掉只留警告:这是调试辅助,绝不能反过来弄崩取数
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import re
15
+ from pathlib import Path
16
+ from urllib.parse import parse_qsl, urlparse
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # 剔除的查询参数:每次请求都变的随机值,留着文件名就永远不重名
21
+ _VOLATILE_PARAMS = {"rand", "_", "t", "ts", "_t", "timestamp", "nonce", "callback"}
22
+
23
+
24
+ def active_dir() -> Path | None:
25
+ d = os.environ.get("FEEDSCOPE_SAMPLE_DIR")
26
+ return Path(d) if d else None
27
+
28
+
29
+ def url_name(source: str, url: str) -> str:
30
+ """URL -> 稳定样本文件名(不含扩展名),如 x--TweetDetail___variables=….json。
31
+
32
+ 查询参数排序 + 随机参数剔除,保证同一端点重跑同名(覆盖刷新)。
33
+ 源标签只保留 ASCII 字符(跨平台文件名安全,非 ASCII 全清后回退 src)。
34
+ """
35
+ u = urlparse(url)
36
+ keep = "&".join(
37
+ f"{k}={v}" for k, v in sorted(parse_qsl(u.query))
38
+ if k not in _VOLATILE_PARAMS and len(v) <= 40
39
+ )
40
+ raw = u.path.strip("/") + ("_" + keep if keep else "")
41
+ clean = re.sub(r"[^A-Za-z0-9._=-]+", "-", raw)[:100].strip("-.") or "root"
42
+ src = re.sub(r"[^a-z0-9]+", "", source.lower()) or "src"
43
+ return f"{src}--{clean}"
44
+
45
+
46
+ def _suffix(content: str | bytes) -> str:
47
+ """按内容嗅探扩展名:JSON / HTML / 其他。"""
48
+ head = content[:128].lstrip()
49
+ if isinstance(head, bytes):
50
+ head = head.decode("utf-8", "ignore")
51
+ if head.startswith(("{", "[")):
52
+ return ".json"
53
+ if head.startswith("<"):
54
+ return ".html"
55
+ return ".txt"
56
+
57
+
58
+ def save(name: str, content: str | bytes) -> None:
59
+ """写样本进 active_dir(未开启 = 无事发生);失败吞掉只留警告。"""
60
+ d = active_dir()
61
+ if d is None:
62
+ return
63
+ try:
64
+ d.mkdir(parents=True, exist_ok=True)
65
+ text = content if isinstance(content, str) else content.decode("utf-8", "ignore")
66
+ (d / (name + _suffix(content))).write_text(text, encoding="utf-8")
67
+ except OSError as exc:
68
+ logger.warning("样本落盘失败 %s:%s", name, exc)