read-cli 0.2.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.
read/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ 读咚咚 (Read) - 个人知识数据层
3
+
4
+ 本地、私有、可编程的个人知识基础设施。
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from read.core.client import Client
10
+
11
+ __all__ = ["Client", "__version__"]
read/cli.py ADDED
@@ -0,0 +1,138 @@
1
+ """CLI 主入口
2
+
3
+ 职责:
4
+ - 统一的 JSON 输出
5
+ - 统一的错误处理
6
+ - Typer 应用配置
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ import traceback
14
+ import typer
15
+ from typing import Any
16
+
17
+ from dong import json_output, ValidationError, NotFoundError, ConflictError
18
+ from read import __version__
19
+
20
+ app = typer.Typer(
21
+ name="read",
22
+ help="读咚咚 (Read) - 个人知识数据层的命令行接口",
23
+ no_args_is_help=True,
24
+ add_completion=False,
25
+ )
26
+
27
+
28
+ def output(data: Any, success: bool = True) -> None:
29
+ """输出 JSON 格式"""
30
+ result: dict[str, Any] = {"success": success}
31
+ if success:
32
+ result["data"] = data
33
+ else:
34
+ result["error"] = data
35
+ typer.echo(json.dumps(result, ensure_ascii=False, indent=2))
36
+
37
+
38
+ def handle_error(e: Exception) -> None:
39
+ """处理异常并输出结构化错误"""
40
+ error_info: dict[str, str] = {
41
+ "code": type(e).__name__,
42
+ "message": str(e),
43
+ }
44
+
45
+ if "--debug" in sys.argv:
46
+ error_info["traceback"] = traceback.format_exc()
47
+
48
+ output(error_info, success=False)
49
+ raise typer.Exit(code=1)
50
+
51
+
52
+ @app.callback()
53
+ def version_callback(
54
+ ctx: typer.Context,
55
+ version: bool = typer.Option(False, "--version", "-v", help="显示版本号"),
56
+ ) -> None:
57
+ """版本号回调"""
58
+ if version:
59
+ typer.echo(f"read {__version__}")
60
+ raise typer.Exit()
61
+
62
+
63
+ @app.command()
64
+ @json_output
65
+ def init():
66
+ """初始化数据库"""
67
+ from read.commands.init import cmd_init
68
+ result = cmd_init()
69
+ return result
70
+
71
+
72
+ @app.command()
73
+ @json_output
74
+ def add(
75
+ content: str = typer.Argument(None, help="摘录内容"),
76
+ url: str = typer.Option(None, "--url", "-u", help="链接"),
77
+ source: str = typer.Option(None, "--source", "-s", help="来源备注"),
78
+ type: str = typer.Option("quote", "--type", "-t", help="数据类型(quote/article/code)"),
79
+ ):
80
+ """添加摘录或链接"""
81
+ from read.commands.add import cmd_add
82
+ result = cmd_add(content=content, url=url, source=source, item_type=type)
83
+ return result
84
+
85
+
86
+ @app.command()
87
+ @json_output
88
+ def ls(
89
+ limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
90
+ offset: int = typer.Option(0, "--offset", "-o", help="偏移量"),
91
+ type: str = typer.Option(None, "--type", "-t", help="筛选类型(content/link)"),
92
+ order: str = typer.Option("desc", "--order", help="排序方向(desc/asc)"),
93
+ ):
94
+ """列出所有摘录"""
95
+ from read.commands.ls import cmd_ls
96
+ result = cmd_ls(limit=limit, offset=offset, item_type=type, order=order)
97
+ return result
98
+
99
+
100
+ @app.command("get")
101
+ @json_output
102
+ def get_item(
103
+ item_id: int = typer.Argument(..., help="摘录 ID"),
104
+ field: str = typer.Option(None, "--field", "-f", help="只返回指定字段"),
105
+ ):
106
+ """获取单条摘录"""
107
+ from read.commands.get import cmd_get
108
+ result = cmd_get(item_id=item_id, field=field)
109
+ return result
110
+
111
+
112
+ @app.command()
113
+ @json_output
114
+ def delete(
115
+ item_ids: list[int] = typer.Argument(..., help="摘录 ID(支持多个)"),
116
+ force: bool = typer.Option(False, "--force", "-f", help="强制删除,不确认"),
117
+ ):
118
+ """删除摘录"""
119
+ from read.commands.delete import cmd_delete
120
+ result = cmd_delete(item_ids=item_ids, force=force)
121
+ return result
122
+
123
+
124
+ @app.command()
125
+ @json_output
126
+ def search(
127
+ query: str = typer.Argument(..., help="搜索关键词"),
128
+ field: str = typer.Option(None, "--field", "-f", help="搜索字段(content/url/source)"),
129
+ limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
130
+ ):
131
+ """搜索摘录"""
132
+ from read.commands.search import cmd_search
133
+ result = cmd_search(query=query, field=field, limit=limit)
134
+ return result
135
+
136
+
137
+ if __name__ == "__main__":
138
+ app()
@@ -0,0 +1,17 @@
1
+ """CLI 命令实现"""
2
+
3
+ from read.commands.init import cmd_init
4
+ from read.commands.add import cmd_add
5
+ from read.commands.ls import cmd_ls
6
+ from read.commands.get import cmd_get
7
+ from read.commands.delete import cmd_delete
8
+ from read.commands.search import cmd_search
9
+
10
+ __all__ = [
11
+ "cmd_init",
12
+ "cmd_add",
13
+ "cmd_ls",
14
+ "cmd_get",
15
+ "cmd_delete",
16
+ "cmd_search",
17
+ ]
read/commands/add.py ADDED
@@ -0,0 +1,32 @@
1
+ """add 命令 - 添加摘录"""
2
+
3
+ from typing import Optional
4
+
5
+ from read.core.client import Client
6
+
7
+
8
+ def cmd_add(
9
+ content: Optional[str],
10
+ url: Optional[str],
11
+ source: Optional[str],
12
+ item_type: str = "quote",
13
+ ) -> dict:
14
+ """添加摘录
15
+
16
+ Args:
17
+ content: 摘录内容
18
+ url: 链接
19
+ source: 来源备注
20
+ item_type: 数据类型
21
+
22
+ Returns:
23
+ 添加结果
24
+ """
25
+ client = Client()
26
+ item = client.add(
27
+ content=content,
28
+ url=url,
29
+ source=source,
30
+ item_type=item_type,
31
+ )
32
+ return item.to_dict()
@@ -0,0 +1,49 @@
1
+ """delete 命令 - 删除摘录"""
2
+
3
+ import typer
4
+ from typing import List
5
+
6
+ from read.core.client import Client
7
+
8
+
9
+ def cmd_delete(item_ids: List[int], force: bool = False) -> dict:
10
+ """删除摘录
11
+
12
+ Args:
13
+ item_ids: 摘录 ID 列表
14
+ force: 是否强制删除
15
+
16
+ Returns:
17
+ 删除结果
18
+ """
19
+ client = Client()
20
+
21
+ if not force and len(item_ids) == 1:
22
+ # 单个删除时确认
23
+ item = client.get_optional(item_ids[0])
24
+ if item:
25
+ confirm = typer.confirm(
26
+ f"确定要删除这条摘录吗?\n\n {item.display_text[:50]}"
27
+ )
28
+ if not confirm:
29
+ return {"deleted": False, "message": "取消删除"}
30
+
31
+ deleted = []
32
+ not_found = []
33
+
34
+ for item_id in item_ids:
35
+ if client.delete(item_id):
36
+ deleted.append(item_id)
37
+ else:
38
+ not_found.append(item_id)
39
+
40
+ result: dict = {
41
+ "deleted": deleted,
42
+ "deleted_count": len(deleted),
43
+ }
44
+
45
+ if not_found:
46
+ result["not_found"] = not_found
47
+ result["not_found_count"] = len(not_found)
48
+
49
+ return result
read/commands/get.py ADDED
@@ -0,0 +1,32 @@
1
+ """get 命令 - 获取单条摘录"""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from read.core.client import Client
6
+ from read.core.client import NotFoundError
7
+
8
+
9
+ def cmd_get(item_id: int, field: Optional[str] = None) -> Any:
10
+ """获取单条摘录
11
+
12
+ Args:
13
+ item_id: 摘录 ID
14
+ field: 只返回指定字段
15
+
16
+ Returns:
17
+ 摘录数据或指定字段值
18
+ """
19
+ client = Client()
20
+
21
+ try:
22
+ item = client.get(item_id)
23
+ except NotFoundError:
24
+ return {
25
+ "error": "not_found",
26
+ "message": f"Item {item_id} not found",
27
+ }
28
+
29
+ if field:
30
+ return item.to_dict().get(field)
31
+
32
+ return item.to_dict()
read/commands/init.py ADDED
@@ -0,0 +1,12 @@
1
+ """init 命令 - 初始化数据库"""
2
+
3
+ from read.db.schema import init_db
4
+
5
+
6
+ def cmd_init() -> dict:
7
+ """初始化数据库
8
+
9
+ Returns:
10
+ 初始化结果
11
+ """
12
+ return init_db()
read/commands/ls.py ADDED
@@ -0,0 +1,47 @@
1
+ """ls 命令 - 列出所有摘录"""
2
+
3
+ from typing import Optional
4
+
5
+ from read.core.client import Client
6
+ from read.db.utils import count_total
7
+
8
+
9
+ def cmd_ls(
10
+ limit: int = 20,
11
+ offset: int = 0,
12
+ item_type: Optional[str] = None,
13
+ order: str = "desc",
14
+ ) -> dict:
15
+ """列出摘录
16
+
17
+ Args:
18
+ limit: 返回数量
19
+ offset: 偏移量
20
+ item_type: 筛选类型
21
+ order: 排序方向
22
+
23
+ Returns:
24
+ 列表结果
25
+ """
26
+ client = Client()
27
+
28
+ # 类型映射
29
+ type_map = {
30
+ "content": "quote",
31
+ "link": "article",
32
+ "code": "code",
33
+ }
34
+ db_type = type_map.get(item_type, item_type) if item_type else None
35
+
36
+ items = client.list(
37
+ limit=limit,
38
+ offset=offset,
39
+ item_type=db_type,
40
+ order=order,
41
+ )
42
+
43
+ return {
44
+ "total": count_total(),
45
+ "count": len(items),
46
+ "items": [item.to_dict() for item in items],
47
+ }
@@ -0,0 +1,31 @@
1
+ """search 命令 - 搜索摘录"""
2
+
3
+ from typing import Optional
4
+
5
+ from read.core.client import Client
6
+
7
+
8
+ def cmd_search(
9
+ query: str,
10
+ field: Optional[str] = None,
11
+ limit: int = 20,
12
+ ) -> dict:
13
+ """搜索摘录
14
+
15
+ Args:
16
+ query: 搜索关键词
17
+ field: 搜索字段
18
+ limit: 返回数量
19
+
20
+ Returns:
21
+ 搜索结果
22
+ """
23
+ client = Client()
24
+ items = client.search(query=query, field=field, limit=limit)
25
+
26
+ return {
27
+ "query": query,
28
+ "field": field,
29
+ "count": len(items),
30
+ "items": [item.to_dict() for item in items],
31
+ }
read/config.py ADDED
@@ -0,0 +1,36 @@
1
+ """配置管理模块
2
+
3
+ 继承 dong.config.Config,管理 read-cli 的用户配置。
4
+ """
5
+
6
+ from dong.config import Config
7
+
8
+
9
+ class ReadConfig(Config):
10
+ """读咚咚配置类"""
11
+
12
+ @classmethod
13
+ def get_name(cls) -> str:
14
+ return "read"
15
+
16
+ @classmethod
17
+ def get_defaults(cls) -> dict:
18
+ return {
19
+ "default_status": "reading",
20
+ "default_limit": 20,
21
+ "statuses": ["reading", "completed", "abandoned"],
22
+ }
23
+
24
+
25
+ # 便捷函数
26
+ def get_config() -> dict:
27
+ return ReadConfig.load()
28
+
29
+ def get_default_status() -> str:
30
+ return ReadConfig.get("default_status", "reading")
31
+
32
+ def get_default_limit() -> int:
33
+ return ReadConfig.get("default_limit", 20)
34
+
35
+ def get_statuses() -> list:
36
+ return ReadConfig.get("statuses", ["reading", "completed", "abandoned"])
read/const.py ADDED
@@ -0,0 +1,28 @@
1
+ """常量定义"""
2
+
3
+ import datetime
4
+
5
+ VERSION = "0.1.0"
6
+ DB_NAME = "read.db"
7
+ # 数据目录 - 统一放在 ~/.dong/ 下
8
+ from pathlib import Path
9
+ DB_DIR = Path.home() / ".dong" / "read"
10
+
11
+ # 默认配置
12
+ DEFAULT_LIMIT = 20
13
+ DEFAULT_TYPE = "quote"
14
+
15
+ # 数据类型
16
+ TYPE_QUOTE = "quote" # 文字摘录
17
+ TYPE_ARTICLE = "article" # 文章链接
18
+ TYPE_CODE = "code" # 代码片段
19
+
20
+ ALL_TYPES = [TYPE_QUOTE, TYPE_ARTICLE, TYPE_CODE]
21
+
22
+ # 时间格式
23
+ ISO_FORMAT = "%Y-%m-%dT%H:%M:%S"
24
+
25
+
26
+ def get_timestamp() -> str:
27
+ """获取当前时间戳(ISO 8601 格式)"""
28
+ return datetime.datetime.now().strftime(ISO_FORMAT)
read/core/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Core Library - 读咚咚的核心资产
2
+
3
+ 这是 read 的核心层,所有客户端(CLI、MCP、SDK)都通过此层访问数据。
4
+ """
5
+
6
+ from read.core.client import Client
7
+ from read.core.models import Item
8
+
9
+ __all__ = ["Client", "Item"]
read/core/client.py ADDED
@@ -0,0 +1,217 @@
1
+ """Python SDK / Core Library
2
+
3
+ 这是读咚咚的核心资产。所有客户端(CLI、MCP、HTTP API)都通过此层访问数据。
4
+
5
+ 设计原则:
6
+ 1. 独立性 - 不依赖任何客户端
7
+ 2. 完整性 - 包含所有数据操作逻辑
8
+ 3. 稳定性 - API 考虑向后兼容
9
+ 4. 可测试性 - 可独立单元测试
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from read.core.models import Item
18
+ from read.db.utils import (
19
+ add_item as db_add_item,
20
+ count_total as db_count_total,
21
+ delete_item as db_delete_item,
22
+ get_item as db_get_item,
23
+ list_items as db_list_items,
24
+ search_items as db_search_items,
25
+ )
26
+
27
+
28
+ class Client:
29
+ """读咚咚客户端
30
+
31
+ 这是 Core Library 的主要入口点,提供给 Agent 和开发者使用。
32
+
33
+ Example:
34
+ >>> from read import Client
35
+ >>> client = Client()
36
+ >>> item = client.add("开始,就是最好的时机")
37
+ >>> print(item.id)
38
+ 1
39
+ >>> items = client.list(limit=10)
40
+ >>> results = client.search("AI")
41
+ """
42
+
43
+ def __init__(self, db_path: Optional[Path] = None):
44
+ """初始化客户端
45
+
46
+ Args:
47
+ db_path: 数据库路径,None 则使用默认路径 (~/.read/read.db)
48
+ """
49
+ self._db_path = db_path
50
+ # 注:v0.1 使用默认连接,db_path 参数为 v0.2 多数据库支持预留
51
+
52
+ def add(
53
+ self,
54
+ content: Optional[str] = None,
55
+ url: Optional[str] = None,
56
+ source: Optional[str] = None,
57
+ item_type: str = "quote",
58
+ metadata: Optional[str] = None,
59
+ ) -> Item:
60
+ """添加摘录
61
+
62
+ Args:
63
+ content: 摘录内容
64
+ url: 链接
65
+ source: 来源备注
66
+ item_type: 数据类型(quote/article/code)
67
+ metadata: JSON 扩展字段
68
+
69
+ Returns:
70
+ 创建的 Item 对象
71
+
72
+ Raises:
73
+ ValueError: content 和 url 都为空
74
+ """
75
+ item_id = db_add_item(
76
+ content=content,
77
+ url=url,
78
+ source=source,
79
+ item_type=item_type,
80
+ metadata=metadata,
81
+ )
82
+ return self.get(item_id)
83
+
84
+ def list(
85
+ self,
86
+ limit: int = 20,
87
+ offset: int = 0,
88
+ item_type: Optional[str] = None,
89
+ order: str = "desc",
90
+ ) -> list[Item]:
91
+ """列出摘录
92
+
93
+ Args:
94
+ limit: 返回数量限制
95
+ offset: 偏移量
96
+ item_type: 筛选类型
97
+ order: 排序方向(desc/asc)
98
+
99
+ Returns:
100
+ Item 列表
101
+ """
102
+ rows = db_list_items(
103
+ limit=limit,
104
+ offset=offset,
105
+ item_type=item_type,
106
+ order=order,
107
+ )
108
+ return [Item.from_dict(row) for row in rows]
109
+
110
+ def get(self, item_id: int) -> Item:
111
+ """获取单条摘录
112
+
113
+ Args:
114
+ item_id: 摘录 ID
115
+
116
+ Returns:
117
+ Item 对象
118
+
119
+ Raises:
120
+ NotFoundError: 摘录不存在
121
+ """
122
+ row = db_get_item(item_id)
123
+ if row is None:
124
+ raise NotFoundError(f"Item {item_id} not found")
125
+ return Item.from_dict(row)
126
+
127
+ def get_optional(self, item_id: int) -> Optional[Item]:
128
+ """获取单条摘录(不存在返回 None)
129
+
130
+ Args:
131
+ item_id: 摘录 ID
132
+
133
+ Returns:
134
+ Item 对象或 None
135
+ """
136
+ row = db_get_item(item_id)
137
+ return Item.from_dict(row) if row else None
138
+
139
+ def delete(self, item_id: int) -> bool:
140
+ """删除摘录
141
+
142
+ Args:
143
+ item_id: 摘录 ID
144
+
145
+ Returns:
146
+ 是否删除成功
147
+ """
148
+ return db_delete_item(item_id)
149
+
150
+ def search(
151
+ self,
152
+ query: str,
153
+ field: Optional[str] = None,
154
+ limit: int = 20,
155
+ ) -> list[Item]:
156
+ """搜索摘录
157
+
158
+ Args:
159
+ query: 搜索关键词
160
+ field: 搜索字段(content/url/source),None 表示全部
161
+ limit: 返回数量限制
162
+
163
+ Returns:
164
+ 匹配的 Item 列表
165
+ """
166
+ rows = db_search_items(query=query, field=field, limit=limit)
167
+ return [Item.from_dict(row) for row in rows]
168
+
169
+ def count(self) -> int:
170
+ """获取摘录总数
171
+
172
+ Returns:
173
+ 总数
174
+ """
175
+ return db_count_total()
176
+
177
+ # 链式调用支持(为 Agent 提供更友好的 API)
178
+ def search_query(self, query: str) -> "QueryBuilder":
179
+ """开始搜索查询
180
+
181
+ Example:
182
+ >>> client.search_query("AI").limit(5).execute()
183
+ """
184
+ return QueryBuilder(self, query)
185
+
186
+
187
+ class QueryBuilder:
188
+ """查询构建器(链式调用)"""
189
+
190
+ def __init__(self, client: Client, query: str):
191
+ self._client = client
192
+ self._query = query
193
+ self._field: Optional[str] = None
194
+ self._limit = 20
195
+
196
+ def by_field(self, field: str) -> "QueryBuilder":
197
+ """指定搜索字段"""
198
+ self._field = field
199
+ return self
200
+
201
+ def limit(self, limit: int) -> "QueryBuilder":
202
+ """设置返回数量"""
203
+ self._limit = limit
204
+ return self
205
+
206
+ def execute(self) -> list[Item]:
207
+ """执行查询"""
208
+ return self._client.search(
209
+ query=self._query,
210
+ field=self._field,
211
+ limit=self._limit,
212
+ )
213
+
214
+
215
+ class NotFoundError(Exception):
216
+ """摘录不存在异常"""
217
+ pass