klnote 0.1.0__tar.gz → 0.3.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.
Files changed (29) hide show
  1. {klnote-0.1.0/klnote.egg-info → klnote-0.3.0}/PKG-INFO +22 -6
  2. klnote-0.1.0/PKG-INFO → klnote-0.3.0/README.md +105 -100
  3. klnote-0.3.0/klnote/__init__.py +3 -0
  4. klnote-0.3.0/klnote/cli.py +284 -0
  5. klnote-0.3.0/klnote/core/__init__.py +3 -0
  6. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/service.py +71 -2
  7. klnote-0.3.0/klnote/core/utils/time_1.py +21 -0
  8. klnote-0.1.0/README.md → klnote-0.3.0/klnote.egg-info/PKG-INFO +116 -90
  9. {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/requires.txt +1 -0
  10. {klnote-0.1.0 → klnote-0.3.0}/pyproject.toml +2 -1
  11. klnote-0.1.0/klnote/cli.py +0 -188
  12. klnote-0.1.0/klnote/core/__init__.py +0 -0
  13. klnote-0.1.0/klnote/core/schema/__init__.py +0 -0
  14. klnote-0.1.0/klnote/core/utils/time_1.py +0 -8
  15. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/constants.py +0 -0
  16. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/database.py +0 -0
  17. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/exceptions.py +0 -0
  18. {klnote-0.1.0/klnote → klnote-0.3.0/klnote/core/schema}/__init__.py +0 -0
  19. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/schema/schame.py +0 -0
  20. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/__init__.py +0 -0
  21. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/db.py +0 -0
  22. {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/editor.py +0 -0
  23. {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/SOURCES.txt +0 -0
  24. {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/dependency_links.txt +0 -0
  25. {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/entry_points.txt +0 -0
  26. {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/top_level.txt +0 -0
  27. {klnote-0.1.0 → klnote-0.3.0}/setup.cfg +0 -0
  28. {klnote-0.1.0 → klnote-0.3.0}/tests/test_cli.py +0 -0
  29. {klnote-0.1.0 → klnote-0.3.0}/tests/test_service.py +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: klnote
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Stock note management tool
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: click>=8.0.0
8
+ Requires-Dist: prettytable>=3.0.0
8
9
  Provides-Extra: dev
9
10
  Requires-Dist: pytest>=8.0.0; extra == "dev"
10
11
 
@@ -37,12 +38,17 @@ pip install klnote
37
38
  klnote stk ls # 列出所有股票
38
39
  klnote stk add 000001 -n 平安银行 # 添加股票
39
40
  klnote stk get 000001 # 查看股票(支持 id/code/name)
40
- klnote stk delete 000001 # 删除股票
41
+ klnote stk del 000001 # 删除股票
42
+ klnote stk get-meta 000001 # 查看 metadata
43
+ klnote stk set-meta 000001 --name 平安银行 --country CN # 更新 metadata
41
44
 
42
45
  # 笔记操作
43
46
  klnote note add 000001 -s "摘要" -c "内容" # 添加笔记
44
47
  klnote note ls 000001 # 列出笔记
45
- klnote note delete 000001 0 # 删除第0条笔记
48
+ klnote note read 000001 0 # 读取第0条笔记
49
+ klnote note edit 000001 0 -c "新内容" # 编辑笔记
50
+ klnote note edit 000001 0 -e # 弹出 GUI 编辑器
51
+ klnote note del 000001 0 # 删除第0条笔记
46
52
  ```
47
53
 
48
54
  ## 数据库
@@ -62,10 +68,18 @@ klnote --help
62
68
  ### 股票操作 (stk)
63
69
 
64
70
  ```
65
- klnote stk ls # 列出所有股票
71
+ klnote stk ls # 列出所有股票(表格对齐)
66
72
  klnote stk add <code> [-n name] [-d desc] # 添加股票
67
73
  klnote stk get <identifier> # 查看股票详情
68
- klnote stk delete <identifier> # 删除股票
74
+ klnote stk del <identifier> # 删除股票
75
+ klnote stk get-meta <identifier> # 查看 metadata
76
+ klnote stk set-meta <identifier> # 更新 metadata
77
+ ```
78
+
79
+ **输出示例:**
80
+ ```
81
+ id code name description note_num created_at updated_at
82
+ 38578199c19644ebab18ce7b5c16d699 688102 斯瑞新财 None 1 2026-06-04T15:52:20.853674Z None
69
83
  ```
70
84
 
71
85
  ### 笔记操作 (note)
@@ -73,7 +87,9 @@ klnote stk delete <identifier> # 删除股票
73
87
  ```
74
88
  klnote note add <stock_identifier> [-s 摘要] [-c 内容] # 添加笔记
75
89
  klnote note ls <stock_identifier> # 列出某只股票的所有笔记
76
- klnote note delete <stock_identifier> <index> # 删除指定笔记
90
+ klnote note read <stock_identifier> <index> # 读取单条笔记
91
+ klnote note edit <stock_identifier> <index> [-c 内容] [-s 摘要] [-e] # 编辑笔记
92
+ klnote note del <stock_identifier> <index> # 删除指定笔记
77
93
  ```
78
94
 
79
95
  ### 高级操作 (admin)
@@ -1,100 +1,105 @@
1
- Metadata-Version: 2.4
2
- Name: klnote
3
- Version: 0.1.0
4
- Summary: Stock note management tool
5
- Requires-Python: >=3.10
6
- Description-Content-Type: text/markdown
7
- Requires-Dist: click>=8.0.0
8
- Provides-Extra: dev
9
- Requires-Dist: pytest>=8.0.0; extra == "dev"
10
-
11
- # KLNote
12
-
13
- 股票笔记管理工具,为专注股票投资的人群设计。
14
-
15
- ## 背景
16
-
17
- 投资股票时,每次决策都需要记录:为什么买、为什么卖、当时想到了什么。这些记录是后续复盘的核心素材。但普通笔记软件太通用,无法围绕"股票"这个实体组织信息——查一只股票的笔记,要翻半天。
18
-
19
- KLNote 的核心思路是:**先有股票,再有笔记**。每条笔记属于某只股票,每次查看都围绕股票展开。
20
-
21
- ## 核心概念
22
-
23
- - **股票**:用 code(代码)或 name(名称)标识,如 `000001`、平安银行
24
- - **笔记**:附属于某只股票,每条包含 summary(摘要) 和 content(内容)
25
- - **标识符**:支持三种方式定位股票 — UUID、股票代码、股票名称
26
-
27
- ## 安装
28
-
29
- ```bash
30
- pip install klnote
31
- ```
32
-
33
- ## 使用
34
-
35
- ```bash
36
- # 股票操作
37
- klnote stk ls # 列出所有股票
38
- klnote stk add 000001 -n 平安银行 # 添加股票
39
- klnote stk get 000001 # 查看股票(支持 id/code/name)
40
- klnote stk delete 000001 # 删除股票
41
-
42
- # 笔记操作
43
- klnote note add 000001 -s "摘要" -c "内容" # 添加笔记
44
- klnote note ls 000001 # 列出笔记
45
- klnote note delete 000001 0 # 删除第0条笔记
46
- ```
47
-
48
- ## 数据库
49
-
50
- 默认路径: `~/.KLNote/KLNote.db`
51
-
52
- 可通过环境变量 `KLNOTE_DB_PATH` 自定义路径。
53
-
54
- ## 命令指南
55
-
56
- ### 全局命令
57
-
58
- ```
59
- klnote --help
60
- ```
61
-
62
- ### 股票操作 (stk)
63
-
64
- ```
65
- klnote stk ls # 列出所有股票
66
- klnote stk add <code> [-n name] [-d desc] # 添加股票
67
- klnote stk get <identifier> # 查看股票详情
68
- klnote stk delete <identifier> # 删除股票
69
- ```
70
-
71
- ### 笔记操作 (note)
72
-
73
- ```
74
- klnote note add <stock_identifier> [-s 摘要] [-c 内容] # 添加笔记
75
- klnote note ls <stock_identifier> # 列出某只股票的所有笔记
76
- klnote note delete <stock_identifier> <index> # 删除指定笔记
77
- ```
78
-
79
- ### 高级操作 (admin)
80
-
81
- ```
82
- klnote admin match_and_read <value> <field> # 按字段匹配读取
83
- klnote admin match_and_update <value> <field> <update_field> # 按字段匹配更新(危险)
84
- ```
85
-
86
- ### 标识符说明
87
-
88
- 所有 `<identifier>` 位置均支持三种写法:
89
-
90
- | 格式 | 示例 | 解析方式 |
91
- |------|------|---------|
92
- | UUID (32位十六进制) | `a1b2c3d4e5f6...` | 精确匹配 id |
93
- | 纯数字 | `000001` | 匹配 code |
94
- | 字母/中文混合 | `平安银行` | 先试 code,再试 name |
95
-
96
- ## 环境变量
97
-
98
- | 变量 | 说明 | 默认值 |
99
- |------|------|--------|
100
- | `KLNOTE_DB_PATH` | 数据库文件路径 | `~/.KLNote/KLNote.db` |
1
+ # KLNote
2
+
3
+ 股票笔记管理工具,为专注股票投资的人群设计。
4
+
5
+ ## 背景
6
+
7
+ 投资股票时,每次决策都需要记录:为什么买、为什么卖、当时想到了什么。这些记录是后续复盘的核心素材。但普通笔记软件太通用,无法围绕"股票"这个实体组织信息——查一只股票的笔记,要翻半天。
8
+
9
+ KLNote 的核心思路是:**先有股票,再有笔记**。每条笔记属于某只股票,每次查看都围绕股票展开。
10
+
11
+ ## 核心概念
12
+
13
+ - **股票**:用 code(代码)或 name(名称)标识,如 `000001`、平安银行
14
+ - **笔记**:附属于某只股票,每条包含 summary(摘要) 和 content(内容)
15
+ - **标识符**:支持三种方式定位股票 — UUID、股票代码、股票名称
16
+
17
+ ## 安装
18
+
19
+ ```bash
20
+ pip install klnote
21
+ ```
22
+
23
+ ## 使用
24
+
25
+ ```bash
26
+ # 股票操作
27
+ klnote stk ls # 列出所有股票
28
+ klnote stk add 000001 -n 平安银行 # 添加股票
29
+ klnote stk get 000001 # 查看股票(支持 id/code/name)
30
+ klnote stk del 000001 # 删除股票
31
+ klnote stk get-meta 000001 # 查看 metadata
32
+ klnote stk set-meta 000001 --name 平安银行 --country CN # 更新 metadata
33
+
34
+ # 笔记操作
35
+ klnote note add 000001 -s "摘要" -c "内容" # 添加笔记
36
+ klnote note ls 000001 # 列出笔记
37
+ klnote note read 000001 0 # 读取第0条笔记
38
+ klnote note edit 000001 0 -c "新内容" # 编辑笔记
39
+ klnote note edit 000001 0 -e # 弹出 GUI 编辑器
40
+ klnote note del 000001 0 # 删除第0条笔记
41
+ ```
42
+
43
+ ## 数据库
44
+
45
+ 默认路径: `~/.KLNote/KLNote.db`
46
+
47
+ 可通过环境变量 `KLNOTE_DB_PATH` 自定义路径。
48
+
49
+ ## 命令指南
50
+
51
+ ### 全局命令
52
+
53
+ ```
54
+ klnote --help
55
+ ```
56
+
57
+ ### 股票操作 (stk)
58
+
59
+ ```
60
+ klnote stk ls # 列出所有股票(表格对齐)
61
+ klnote stk add <code> [-n name] [-d desc] # 添加股票
62
+ klnote stk get <identifier> # 查看股票详情
63
+ klnote stk del <identifier> # 删除股票
64
+ klnote stk get-meta <identifier> # 查看 metadata
65
+ klnote stk set-meta <identifier> # 更新 metadata
66
+ ```
67
+
68
+ **输出示例:**
69
+ ```
70
+ id code name description note_num created_at updated_at
71
+ 38578199c19644ebab18ce7b5c16d699 688102 斯瑞新财 None 1 2026-06-04T15:52:20.853674Z None
72
+ ```
73
+
74
+ ### 笔记操作 (note)
75
+
76
+ ```
77
+ klnote note add <stock_identifier> [-s 摘要] [-c 内容] # 添加笔记
78
+ klnote note ls <stock_identifier> # 列出某只股票的所有笔记
79
+ klnote note read <stock_identifier> <index> # 读取单条笔记
80
+ klnote note edit <stock_identifier> <index> [-c 内容] [-s 摘要] [-e] # 编辑笔记
81
+ klnote note del <stock_identifier> <index> # 删除指定笔记
82
+ ```
83
+
84
+ ### 高级操作 (admin)
85
+
86
+ ```
87
+ klnote admin match_and_read <value> <field> # 按字段匹配读取
88
+ klnote admin match_and_update <value> <field> <update_field> # 按字段匹配更新(危险)
89
+ ```
90
+
91
+ ### 标识符说明
92
+
93
+ 所有 `<identifier>` 位置均支持三种写法:
94
+
95
+ | 格式 | 示例 | 解析方式 |
96
+ |------|------|---------|
97
+ | UUID (32位十六进制) | `a1b2c3d4e5f6...` | 精确匹配 id |
98
+ | 纯数字 | `000001` | 匹配 code |
99
+ | 字母/中文混合 | `平安银行` | 先试 code,再试 name |
100
+
101
+ ## 环境变量
102
+
103
+ | 变量 | 说明 | 默认值 |
104
+ |------|------|--------|
105
+ | `KLNOTE_DB_PATH` | 数据库文件路径 | `~/.KLNote/KLNote.db` |
@@ -0,0 +1,3 @@
1
+ __version__ = "0.3.0"
2
+ __author__ = "zimvir"
3
+ __email__ = "zimvir@qq.com"
@@ -0,0 +1,284 @@
1
+
2
+ import click
3
+
4
+ from prettytable import PrettyTable
5
+
6
+ from .core import Service
7
+ from .core.utils.time_1 import format_iso
8
+
9
+
10
+ service = Service()
11
+
12
+
13
+ @click.group()
14
+ def cli():
15
+ """KLNote - 股票笔记管理"""
16
+ pass
17
+
18
+
19
+
20
+
21
+ # ============= stock 组 =============
22
+ @cli.group()
23
+ def stk():
24
+ """股票操作"""
25
+ pass
26
+
27
+
28
+ @stk.command()
29
+ def ls():
30
+ """列出所有股票"""
31
+ stocks = service.stock_list()
32
+ if not stocks:
33
+ click.echo("暂无股票")
34
+ return
35
+ headers = ['id', 'code', 'name', 'description', 'note_num', 'created_at', 'updated_at']
36
+ pt = PrettyTable(headers)
37
+ pt.align = "l"
38
+ pt.horizontal_char = '-'
39
+ pt.border = False
40
+
41
+ rows =[[
42
+ s.get("id") or "None",
43
+ s.get("code") or "None",
44
+ s.get("name") or "None",
45
+ s.get("description") or "None",
46
+ str(len(s.get("notes") or [])),
47
+ s.get("created_at") or "None",
48
+ s.get("updated_at") or "None"
49
+ ]for s in stocks]
50
+ pt.add_rows(rows)
51
+ click.echo(pt)
52
+
53
+
54
+ @stk.command()
55
+ @click.argument('code')
56
+ @click.option('-n', '--name', default='', help='股票名称')
57
+ @click.option('-d', '--description', default='', help='描述')
58
+ def add(code, name, description):
59
+ """新增股票"""
60
+ service.stock_add(code=code, name=name, description=description)
61
+ click.echo(f"已添加: {code}")
62
+
63
+
64
+ @stk.command()
65
+ @click.argument('identifier')
66
+ def get(identifier):
67
+ """查看股票详情(支持 id/code/name)"""
68
+ stock_ids = service.match_by_identifier(identifier)
69
+ if not stock_ids:
70
+ click.echo("未找到")
71
+ return
72
+ for stock_id in stock_ids:
73
+ s = service.stock_show(stock_id)
74
+ click.echo(f"ID: {s['id']}")
75
+ click.echo(f"代码: {s['code']}")
76
+ click.echo(f"名称: {s.get('name', '')}")
77
+ click.echo(f"描述: {s.get('description', '')}")
78
+ notes = s.get('notes', [])
79
+ click.echo(f"笔记数: {len(notes)}")
80
+ click.echo("-"*50)
81
+
82
+
83
+ @stk.command()
84
+ @click.argument('identifier')
85
+ def get_meta(identifier):
86
+ """查看股票 metadata(支持 id/code/name)"""
87
+ stock_ids = service.match_by_identifier(identifier)
88
+ if not stock_ids:
89
+ click.echo("未找到")
90
+ return
91
+ for stock_id in stock_ids:
92
+ s = service.stock_show(stock_id)
93
+ metadata = s.get("metadata", {})
94
+ click.echo(f"ID: {stock_id}")
95
+ for k, v in metadata.items():
96
+ click.echo(f" {k}: {v}")
97
+ click.echo("-"*50)
98
+
99
+
100
+ @stk.command()
101
+ @click.argument('identifier')
102
+ @click.option('--code', default=None, help='股票代码')
103
+ @click.option('--name', default=None, help='股票名称')
104
+ @click.option('--country', default=None, help='国家')
105
+ @click.option('--label', default=None, help='标签, 如中国A股:A')
106
+ def set_meta(identifier, code, name, country, label):
107
+ """更新股票 metadata(支持 id/code/name)"""
108
+ stock_ids = service.match_by_identifier(identifier)
109
+ if not stock_ids:
110
+ click.echo("未找到")
111
+ return
112
+ for stock_id in stock_ids:
113
+ kwargs = {}
114
+ if code is not None:
115
+ kwargs["code"] = code
116
+ if name is not None:
117
+ kwargs["name"] = name
118
+ if country is not None:
119
+ kwargs["country"] = country
120
+ if label is not None:
121
+ kwargs["label"] = label
122
+ if not kwargs:
123
+ click.echo("请至少指定一个要更新的字段")
124
+ continue
125
+ service.update_metadata(stock_id, **kwargs)
126
+ click.echo(f"已更新 metadata")
127
+ click.echo("-"*50)
128
+
129
+
130
+ @stk.command('del')
131
+ @click.argument('identifier')
132
+ def delete(identifier):
133
+ """删除股票(支持 id/code/name)"""
134
+ stock_ids = service.match_by_identifier(identifier)
135
+ if not stock_ids:
136
+ click.echo("未找到")
137
+ return
138
+ for stock_id in stock_ids:
139
+ service.stock_delete(stock_id)
140
+ click.echo(f"已删除: {stock_id}")
141
+ click.echo("-" * 50)
142
+
143
+ # ============= note 组 =============
144
+ @cli.group()
145
+ def note():
146
+ """笔记操作"""
147
+ pass
148
+
149
+
150
+ @note.command()
151
+ @click.argument('stock_identifier')
152
+ @click.option('-s', '--summary', default='', help='笔记摘要')
153
+ @click.option('-c', '--content', default='', help='笔记内容')
154
+ def add(stock_identifier, summary, content):
155
+ """添加笔记(支持 id/code/name)"""
156
+ stock_ids = service.match_by_identifier(stock_identifier)
157
+ if not stock_ids:
158
+ click.echo("未找到股票")
159
+ return
160
+ for stock_id in stock_ids:
161
+ service.note_add(stock_id, content=content, summary=summary)
162
+ click.echo(f"'id' = '{stock_id}' 笔记已添加")
163
+ click.echo("-" * 50)
164
+
165
+ @note.command()
166
+ @click.argument('stock_identifier')
167
+ def ls(stock_identifier):
168
+ """列出股票的所有笔记(支持 id/code/name)"""
169
+ stock_ids = service.match_by_identifier(stock_identifier)
170
+ if not stock_ids:
171
+ click.echo("未找到股票")
172
+ return
173
+ for stock_id in stock_ids:
174
+ notes = service.note_read(stock_id)
175
+ if not notes:
176
+ click.echo(f"[{stock_id}] 暂无笔记")
177
+ continue
178
+ for i, n in enumerate(notes):
179
+ click.echo(f"索引: {i}")
180
+ click.echo(f"创建时间: {format_iso(n.get('created_at', ''))}")
181
+ click.echo(f"更新时间: {format_iso(n.get('updated_at', ''))}")
182
+ click.echo(f"摘要: {n.get('summary', '')}")
183
+ if n.get('content'):
184
+ click.echo(f"内容: \n {n.get('content', '')}")
185
+
186
+ click.echo("-"*50)
187
+
188
+
189
+ @note.command()
190
+ @click.argument('stock_identifier')
191
+ @click.argument('index', type=int)
192
+ def read(stock_identifier, index):
193
+ """读取单条笔记(支持 id/code/name)"""
194
+ stock_ids = service.match_by_identifier(stock_identifier)
195
+ if not stock_ids:
196
+ click.echo("未找到股票")
197
+ return
198
+ for stock_id in stock_ids:
199
+ notes = service.note_read(stock_id)
200
+ if index < 0 or index >= len(notes):
201
+ click.echo(f"索引 {index} 不存在")
202
+ continue
203
+ n = notes[index]
204
+ click.echo(f"索引: {index}")
205
+ click.echo(f"创建时间: {format_iso(n.get('created_at', ''))}")
206
+ click.echo(f"更新时间: {format_iso(n.get('updated_at', ''))}")
207
+
208
+ click.echo(f"摘要: {n.get('summary', '')}")
209
+ if n.get('content'):
210
+ click.echo(f"内容:\n {n.get('content', '')}")
211
+
212
+ click.echo("-"*50)
213
+
214
+
215
+ @note.command('del')
216
+ @click.argument('stock_identifier')
217
+ @click.argument('index', type=int)
218
+ def delete(stock_identifier, index):
219
+ """删除笔记(支持 id/code/name)"""
220
+ stock_ids = service.match_by_identifier(stock_identifier)
221
+ if not stock_ids:
222
+ click.echo("未找到股票")
223
+ return
224
+ for stock_id in stock_ids:
225
+ service.note_delete(stock_id, index)
226
+ click.echo(f"已删除第 {index} 条笔记")
227
+ click.echo("-" * 50)
228
+
229
+
230
+ @note.command()
231
+ @click.argument('stock_identifier')
232
+ @click.argument('index', type=int)
233
+ @click.option('-s', '--summary', default=None, help='笔记摘要')
234
+ @click.option('-c', '--content', default=None, help='笔记内容')
235
+ @click.option('-e', '--editer', is_flag=True, help='弹出 GUI 编辑器')
236
+ def edit(stock_identifier, index, summary, content, editer):
237
+ """编辑笔记(支持 id/code/name)"""
238
+ if not content and not summary and not editer:
239
+ click.echo("请指定 --content 或 --summary 或 --editer")
240
+ return
241
+ stock_ids = service.match_by_identifier(stock_identifier)
242
+ if not stock_ids:
243
+ click.echo("未找到股票")
244
+ return
245
+ for stock_id in stock_ids:
246
+ service.note_edit(stock_id, index, content=content, summary=summary, editer=editer)
247
+ click.echo(f"已编辑第 {index} 条笔记")
248
+ click.echo("-" * 50)
249
+
250
+ # ============= admin 组 =============
251
+ @cli.group()
252
+ def admin():
253
+ """高级操作(直接操作数据库字段)"""
254
+ pass
255
+
256
+
257
+ @admin.command()
258
+ @click.argument('value')
259
+ @click.argument('field')
260
+ def match_and_read(value, field):
261
+ """按字段匹配并读取所有匹配的股票"""
262
+ results = service.match_and_read(value, field)
263
+ if not results:
264
+ click.echo("未找到匹配结果")
265
+ return
266
+ for stock_id, data in results.items():
267
+ click.echo(f"ID: {stock_id}")
268
+ click.echo(f" code: {data.get('code', '')}")
269
+ click.echo(f" name: {data.get('name', '')}")
270
+ click.echo("-" * 50)
271
+
272
+
273
+ @admin.command()
274
+ @click.argument('value')
275
+ @click.argument('field')
276
+ @click.argument('update_field')
277
+ def match_and_update(value, field, update_field):
278
+ """按字段匹配并更新(危险操作)"""
279
+ service.match_and_update(value, field, update_field)
280
+ click.echo(f"已更新 field={update_field} 的所有匹配记录")
281
+
282
+
283
+ if __name__ == '__main__':
284
+ cli()
@@ -0,0 +1,3 @@
1
+ from .service import Service
2
+
3
+
@@ -1,5 +1,5 @@
1
1
  import json
2
-
2
+ import re
3
3
  from .database import Database
4
4
  from .constants import DB_PATH
5
5
  from .utils import get_iso_timestamp
@@ -10,7 +10,8 @@ class Service:
10
10
  self.db = db or Database(DB_PATH)
11
11
 
12
12
 
13
- def search(self,id:str=None, code:str=None, name:str=None):
13
+
14
+ def match_by_id_code_name(self,id:str=None, code:str=None, name:str=None):
14
15
  """
15
16
  id code name 三选一 查询,若多填, 优先 找 id 其次 code 最后 name
16
17
 
@@ -26,10 +27,49 @@ class Service:
26
27
 
27
28
  return ids
28
29
 
30
+ def match_by_identifier(self,identifier: str):
31
+ """
32
+ 股票标识符解析,返回 stock id 列表
33
+
34
+ 解析逻辑:
35
+ 1. UUID格式(32位十六进制) → search(id=)
36
+ 2. 纯数字 → search(code=)
37
+ 3. 其他(字母/中文混合) → search(code=) 优先,search(name=) 兜底
38
+ """
39
+
40
+ # UUID → id
41
+ if re.fullmatch(r'[0-9a-f]{32}', identifier):
42
+ return self.match_by_id_code_name(id=identifier)
43
+
44
+ # 纯数字 → code
45
+ if identifier.isdigit():
46
+ return self.match_by_id_code_name(code=identifier)
47
+
48
+ # 其他(字母混合)→ 先试 code,再试 name
49
+ ids = self.match_by_id_code_name(code=identifier)
50
+ if not ids:
51
+ ids = self.match_by_id_code_name(name=identifier)
52
+ return ids
53
+
29
54
  # stock
30
55
  def stock_add(self, code: str, id: str = None, name: str = None, description: str = None):
31
56
  """新建股票条目"""
32
57
  self.db.create_and_add_new_stock_row(code=code, id=id, name=name, description=description)
58
+ stock_ids = self.match_by_id_code_name(id=id, code=code, name=name)
59
+ if stock_ids:
60
+ self.update_metadata(stock_ids[0], code=code, name=name)
61
+ return self
62
+
63
+ def update_metadata(self, stock_id: str, **kwargs):
64
+ """更新股票的 metadata 字段
65
+
66
+ :param stock_id: 股票 ID
67
+ :param kwargs: 任意 key=value,如 code="000001", name="平安银行", country="CN"
68
+ """
69
+ stock = self.stock_show(stock_id)
70
+ metadata = stock.get("metadata", {})
71
+ metadata.update(kwargs)
72
+ self.db.update(stock_id, "metadata", json.dumps(metadata))
33
73
  return self
34
74
 
35
75
  def stock_delete(self, id: str):
@@ -57,6 +97,10 @@ class Service:
57
97
  "extra": extra or {}
58
98
  }
59
99
 
100
+ def touch_updated_at(self, stock_id: str):
101
+ """更新股票的 updated_at 时间戳"""
102
+ self.db.update(stock_id, "updated_at", get_iso_timestamp())
103
+
60
104
  def note_add(self, stock_id: str, content: str, summary: str = None, extra: dict = None):
61
105
  """给股票添加一条笔记"""
62
106
  stock = self.stock_show(stock_id)
@@ -64,6 +108,7 @@ class Service:
64
108
  note = self._build_note(content, summary, extra)
65
109
  notes.append(note)
66
110
  self.db.update(stock_id, "notes", json.dumps(notes))
111
+ self.touch_updated_at(stock_id)
67
112
  return self
68
113
 
69
114
  def note_delete(self, stock_id: str, index: int):
@@ -72,6 +117,30 @@ class Service:
72
117
  notes = stock.get("notes", [])
73
118
  notes.pop(index)
74
119
  self.db.update(stock_id, "notes", json.dumps(notes))
120
+ self.touch_updated_at(stock_id)
121
+ return self
122
+
123
+ def note_edit(self, stock_id: str, index: int, content: str = None, summary: str = None, editer: bool = False):
124
+ """编辑股票指定索引的笔记"""
125
+ stock = self.stock_show(stock_id)
126
+ notes = stock.get("notes", [])
127
+ if index < 0 or index >= len(notes):
128
+ raise IndexError(f"笔记索引 {index} 不存在")
129
+ if editer:
130
+ from .utils.editor import edit_in_editor
131
+ edited = edit_in_editor(initial_content=notes[index].get("content") or "")
132
+ if edited is None:
133
+ return self
134
+ content = edited
135
+ now = get_iso_timestamp()
136
+ if content is not None:
137
+ notes[index]["content"] = content
138
+ notes[index]["updated_at"] = now
139
+ if summary is not None:
140
+ notes[index]["summary"] = summary
141
+ notes[index]["updated_at"] = now
142
+ self.db.update(stock_id, "notes", json.dumps(notes))
143
+ self.touch_updated_at(stock_id)
75
144
  return self
76
145
 
77
146
  def _read_notes(self, stock_id: str) -> list:
@@ -0,0 +1,21 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+
4
+
5
+ def get_iso_timestamp(microseconds=True):
6
+ """自定义是否包含微秒"""
7
+ if microseconds:
8
+ return datetime.now(timezone.utc).isoformat(timespec='microseconds').replace('+00:00', 'Z')
9
+ else:
10
+ return datetime.now(timezone.utc).isoformat(timespec='seconds').replace('+00:00', 'Z')
11
+
12
+
13
+ def format_iso(iso_str: str | None) -> str:
14
+ """将 ISO 时间戳解码为本地可读格式,如 '2026-06-09 14:30:00'"""
15
+ if not iso_str:
16
+ return ""
17
+ try:
18
+ dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
19
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
20
+ except (ValueError, TypeError):
21
+ return iso_str
@@ -1,90 +1,116 @@
1
- # KLNote
2
-
3
- 股票笔记管理工具,为专注股票投资的人群设计。
4
-
5
- ## 背景
6
-
7
- 投资股票时,每次决策都需要记录:为什么买、为什么卖、当时想到了什么。这些记录是后续复盘的核心素材。但普通笔记软件太通用,无法围绕"股票"这个实体组织信息——查一只股票的笔记,要翻半天。
8
-
9
- KLNote 的核心思路是:**先有股票,再有笔记**。每条笔记属于某只股票,每次查看都围绕股票展开。
10
-
11
- ## 核心概念
12
-
13
- - **股票**:用 code(代码)或 name(名称)标识,如 `000001`、平安银行
14
- - **笔记**:附属于某只股票,每条包含 summary(摘要) 和 content(内容)
15
- - **标识符**:支持三种方式定位股票 — UUID、股票代码、股票名称
16
-
17
- ## 安装
18
-
19
- ```bash
20
- pip install klnote
21
- ```
22
-
23
- ## 使用
24
-
25
- ```bash
26
- # 股票操作
27
- klnote stk ls # 列出所有股票
28
- klnote stk add 000001 -n 平安银行 # 添加股票
29
- klnote stk get 000001 # 查看股票(支持 id/code/name)
30
- klnote stk delete 000001 # 删除股票
31
-
32
- # 笔记操作
33
- klnote note add 000001 -s "摘要" -c "内容" # 添加笔记
34
- klnote note ls 000001 # 列出笔记
35
- klnote note delete 000001 0 # 删除第0条笔记
36
- ```
37
-
38
- ## 数据库
39
-
40
- 默认路径: `~/.KLNote/KLNote.db`
41
-
42
- 可通过环境变量 `KLNOTE_DB_PATH` 自定义路径。
43
-
44
- ## 命令指南
45
-
46
- ### 全局命令
47
-
48
- ```
49
- klnote --help
50
- ```
51
-
52
- ### 股票操作 (stk)
53
-
54
- ```
55
- klnote stk ls # 列出所有股票
56
- klnote stk add <code> [-n name] [-d desc] # 添加股票
57
- klnote stk get <identifier> # 查看股票详情
58
- klnote stk delete <identifier> # 删除股票
59
- ```
60
-
61
- ### 笔记操作 (note)
62
-
63
- ```
64
- klnote note add <stock_identifier> [-s 摘要] [-c 内容] # 添加笔记
65
- klnote note ls <stock_identifier> # 列出某只股票的所有笔记
66
- klnote note delete <stock_identifier> <index> # 删除指定笔记
67
- ```
68
-
69
- ### 高级操作 (admin)
70
-
71
- ```
72
- klnote admin match_and_read <value> <field> # 按字段匹配读取
73
- klnote admin match_and_update <value> <field> <update_field> # 按字段匹配更新(危险)
74
- ```
75
-
76
- ### 标识符说明
77
-
78
- 所有 `<identifier>` 位置均支持三种写法:
79
-
80
- | 格式 | 示例 | 解析方式 |
81
- |------|------|---------|
82
- | UUID (32位十六进制) | `a1b2c3d4e5f6...` | 精确匹配 id |
83
- | 纯数字 | `000001` | 匹配 code |
84
- | 字母/中文混合 | `平安银行` | 先试 code,再试 name |
85
-
86
- ## 环境变量
87
-
88
- | 变量 | 说明 | 默认值 |
89
- |------|------|--------|
90
- | `KLNOTE_DB_PATH` | 数据库文件路径 | `~/.KLNote/KLNote.db` |
1
+ Metadata-Version: 2.4
2
+ Name: klnote
3
+ Version: 0.3.0
4
+ Summary: Stock note management tool
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.0.0
8
+ Requires-Dist: prettytable>=3.0.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
11
+
12
+ # KLNote
13
+
14
+ 股票笔记管理工具,为专注股票投资的人群设计。
15
+
16
+ ## 背景
17
+
18
+ 投资股票时,每次决策都需要记录:为什么买、为什么卖、当时想到了什么。这些记录是后续复盘的核心素材。但普通笔记软件太通用,无法围绕"股票"这个实体组织信息——查一只股票的笔记,要翻半天。
19
+
20
+ KLNote 的核心思路是:**先有股票,再有笔记**。每条笔记属于某只股票,每次查看都围绕股票展开。
21
+
22
+ ## 核心概念
23
+
24
+ - **股票**:用 code(代码)或 name(名称)标识,如 `000001`、平安银行
25
+ - **笔记**:附属于某只股票,每条包含 summary(摘要) 和 content(内容)
26
+ - **标识符**:支持三种方式定位股票 — UUID、股票代码、股票名称
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install klnote
32
+ ```
33
+
34
+ ## 使用
35
+
36
+ ```bash
37
+ # 股票操作
38
+ klnote stk ls # 列出所有股票
39
+ klnote stk add 000001 -n 平安银行 # 添加股票
40
+ klnote stk get 000001 # 查看股票(支持 id/code/name)
41
+ klnote stk del 000001 # 删除股票
42
+ klnote stk get-meta 000001 # 查看 metadata
43
+ klnote stk set-meta 000001 --name 平安银行 --country CN # 更新 metadata
44
+
45
+ # 笔记操作
46
+ klnote note add 000001 -s "摘要" -c "内容" # 添加笔记
47
+ klnote note ls 000001 # 列出笔记
48
+ klnote note read 000001 0 # 读取第0条笔记
49
+ klnote note edit 000001 0 -c "新内容" # 编辑笔记
50
+ klnote note edit 000001 0 -e # 弹出 GUI 编辑器
51
+ klnote note del 000001 0 # 删除第0条笔记
52
+ ```
53
+
54
+ ## 数据库
55
+
56
+ 默认路径: `~/.KLNote/KLNote.db`
57
+
58
+ 可通过环境变量 `KLNOTE_DB_PATH` 自定义路径。
59
+
60
+ ## 命令指南
61
+
62
+ ### 全局命令
63
+
64
+ ```
65
+ klnote --help
66
+ ```
67
+
68
+ ### 股票操作 (stk)
69
+
70
+ ```
71
+ klnote stk ls # 列出所有股票(表格对齐)
72
+ klnote stk add <code> [-n name] [-d desc] # 添加股票
73
+ klnote stk get <identifier> # 查看股票详情
74
+ klnote stk del <identifier> # 删除股票
75
+ klnote stk get-meta <identifier> # 查看 metadata
76
+ klnote stk set-meta <identifier> # 更新 metadata
77
+ ```
78
+
79
+ **输出示例:**
80
+ ```
81
+ id code name description note_num created_at updated_at
82
+ 38578199c19644ebab18ce7b5c16d699 688102 斯瑞新财 None 1 2026-06-04T15:52:20.853674Z None
83
+ ```
84
+
85
+ ### 笔记操作 (note)
86
+
87
+ ```
88
+ klnote note add <stock_identifier> [-s 摘要] [-c 内容] # 添加笔记
89
+ klnote note ls <stock_identifier> # 列出某只股票的所有笔记
90
+ klnote note read <stock_identifier> <index> # 读取单条笔记
91
+ klnote note edit <stock_identifier> <index> [-c 内容] [-s 摘要] [-e] # 编辑笔记
92
+ klnote note del <stock_identifier> <index> # 删除指定笔记
93
+ ```
94
+
95
+ ### 高级操作 (admin)
96
+
97
+ ```
98
+ klnote admin match_and_read <value> <field> # 按字段匹配读取
99
+ klnote admin match_and_update <value> <field> <update_field> # 按字段匹配更新(危险)
100
+ ```
101
+
102
+ ### 标识符说明
103
+
104
+ 所有 `<identifier>` 位置均支持三种写法:
105
+
106
+ | 格式 | 示例 | 解析方式 |
107
+ |------|------|---------|
108
+ | UUID (32位十六进制) | `a1b2c3d4e5f6...` | 精确匹配 id |
109
+ | 纯数字 | `000001` | 匹配 code |
110
+ | 字母/中文混合 | `平安银行` | 先试 code,再试 name |
111
+
112
+ ## 环境变量
113
+
114
+ | 变量 | 说明 | 默认值 |
115
+ |------|------|--------|
116
+ | `KLNOTE_DB_PATH` | 数据库文件路径 | `~/.KLNote/KLNote.db` |
@@ -1,4 +1,5 @@
1
1
  click>=8.0.0
2
+ prettytable>=3.0.0
2
3
 
3
4
  [dev]
4
5
  pytest>=8.0.0
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "klnote"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "Stock note management tool"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
11
  "click>=8.0.0",
12
+ "prettytable>=3.0.0",
12
13
  ]
13
14
  readme = "README.md"
14
15
 
@@ -1,188 +0,0 @@
1
- import click
2
- from klnote.core.service import Service
3
-
4
- service = Service()
5
-
6
-
7
- @click.group()
8
- def cli():
9
- """KLNote - 股票笔记管理"""
10
- pass
11
-
12
-
13
- def search_by_identifier(identifier: str):
14
- """
15
- 股票标识符解析,返回 stock id 列表
16
-
17
- 解析逻辑:
18
- 1. UUID格式(32位十六进制) → search(id=)
19
- 2. 纯数字 → search(code=)
20
- 3. 其他(字母/中文混合) → search(code=) 优先,search(name=) 兜底
21
- """
22
- import re
23
- # UUID → id
24
- if re.fullmatch(r'[0-9a-f]{32}', identifier):
25
- return service.search(id=identifier)
26
-
27
- # 纯数字 → code
28
- if identifier.isdigit():
29
- return service.search(code=identifier)
30
-
31
- # 其他(字母混合)→ 先试 code,再试 name
32
- ids = service.search(code=identifier)
33
- if not ids:
34
- ids = service.search(name=identifier)
35
- return ids
36
-
37
-
38
- # ============= stock 组 =============
39
- @cli.group()
40
- def stk():
41
- """股票操作"""
42
- pass
43
-
44
-
45
- @stk.command()
46
- def ls():
47
- """列出所有股票"""
48
- stocks = service.stock_list()
49
- if not stocks:
50
- click.echo("暂无股票")
51
- return
52
- for s in stocks:
53
- click.echo(f"{s['id']}: {s['code']} {s.get('name', '')}")
54
-
55
-
56
- @stk.command()
57
- @click.argument('code')
58
- @click.option('-n', '--name', default='', help='股票名称')
59
- @click.option('-d', '--description', default='', help='描述')
60
- def add(code, name, description):
61
- """新增股票"""
62
- service.stock_add(code=code, name=name, description=description)
63
- click.echo(f"已添加: {code}")
64
-
65
-
66
- @stk.command()
67
- @click.argument('identifier')
68
- def get(identifier):
69
- """查看股票详情(支持 id/code/name)"""
70
- stock_ids = search_by_identifier(identifier)
71
- if not stock_ids:
72
- click.echo("未找到")
73
- return
74
- for stock_id in stock_ids:
75
- s = service.stock_show(stock_id)
76
- click.echo(f"ID: {s['id']}")
77
- click.echo(f"代码: {s['code']}")
78
- click.echo(f"名称: {s.get('name', '')}")
79
- click.echo(f"描述: {s.get('description', '')}")
80
- notes = s.get('notes', [])
81
- click.echo(f"笔记数: {len(notes)}")
82
- click.echo("-"*50)
83
-
84
-
85
- @stk.command()
86
- @click.argument('identifier')
87
- def delete(identifier):
88
- """删除股票(支持 id/code/name)"""
89
- stock_ids = search_by_identifier(identifier)
90
- if not stock_ids:
91
- click.echo("未找到")
92
- return
93
- for stock_id in stock_ids:
94
- service.stock_delete(stock_id)
95
- click.echo(f"已删除: {stock_id}")
96
- click.echo("-" * 50)
97
-
98
- # ============= note 组 =============
99
- @cli.group()
100
- def note():
101
- """笔记操作"""
102
- pass
103
-
104
-
105
- @note.command()
106
- @click.argument('stock_identifier')
107
- @click.option('-s', '--summary', default='', help='笔记摘要')
108
- @click.option('-c', '--content', default='', help='笔记内容')
109
- def add(stock_identifier, summary, content):
110
- """添加笔记(支持 id/code/name)"""
111
- stock_ids = search_by_identifier(stock_identifier)
112
- if not stock_ids:
113
- click.echo("未找到股票")
114
- return
115
- for stock_id in stock_ids:
116
- service.note_add(stock_id, content=content, summary=summary)
117
- click.echo(f"'id' = '{stock_id}' 笔记已添加")
118
- click.echo("-" * 50)
119
-
120
- @note.command()
121
- @click.argument('stock_identifier')
122
- def ls(stock_identifier):
123
- """列出股票的所有笔记(支持 id/code/name)"""
124
- stock_ids = search_by_identifier(stock_identifier)
125
- if not stock_ids:
126
- click.echo("未找到股票")
127
- return
128
- for stock_id in stock_ids:
129
- notes = service.note_read(stock_id)
130
- if not notes:
131
- click.echo(f"[{stock_id}] 暂无笔记")
132
- continue
133
- for i, n in enumerate(notes):
134
- click.echo(f"[{i}] {n.get('summary', '')}")
135
- click.echo(f" {n.get('content', '')}")
136
- click.echo(f" created: {n.get('created_at', '')}")
137
- click.echo("-"*50)
138
-
139
-
140
- @note.command()
141
- @click.argument('stock_identifier')
142
- @click.argument('index', type=int)
143
- def delete(stock_identifier, index):
144
- """删除笔记(支持 id/code/name)"""
145
- stock_ids = search_by_identifier(stock_identifier)
146
- if not stock_ids:
147
- click.echo("未找到股票")
148
- return
149
- for stock_id in stock_ids:
150
- service.note_delete(stock_id, index)
151
- click.echo(f"已删除第 {index} 条笔记")
152
- click.echo("-" * 50)
153
-
154
- # ============= admin 组 =============
155
- @cli.group()
156
- def admin():
157
- """高级操作(直接操作数据库字段)"""
158
- pass
159
-
160
-
161
- @admin.command()
162
- @click.argument('value')
163
- @click.argument('field')
164
- def match_and_read(value, field):
165
- """按字段匹配并读取所有匹配的股票"""
166
- results = service.match_and_read(value, field)
167
- if not results:
168
- click.echo("未找到匹配结果")
169
- return
170
- for stock_id, data in results.items():
171
- click.echo(f"ID: {stock_id}")
172
- click.echo(f" code: {data.get('code', '')}")
173
- click.echo(f" name: {data.get('name', '')}")
174
- click.echo("-" * 50)
175
-
176
-
177
- @admin.command()
178
- @click.argument('value')
179
- @click.argument('field')
180
- @click.argument('update_field')
181
- def match_and_update(value, field, update_field):
182
- """按字段匹配并更新(危险操作)"""
183
- service.match_and_update(value, field, update_field)
184
- click.echo(f"已更新 field={update_field} 的所有匹配记录")
185
-
186
-
187
- if __name__ == '__main__':
188
- cli()
File without changes
File without changes
@@ -1,8 +0,0 @@
1
- import os
2
- from datetime import datetime, timezone
3
- def get_iso_timestamp(microseconds=True):
4
- """自定义是否包含微秒"""
5
- if microseconds:
6
- return datetime.now(timezone.utc).isoformat(timespec='microseconds').replace('+00:00', 'Z')
7
- else:
8
- return datetime.now(timezone.utc).isoformat(timespec='seconds').replace('+00:00', 'Z')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes