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.
- {klnote-0.1.0/klnote.egg-info → klnote-0.3.0}/PKG-INFO +22 -6
- klnote-0.1.0/PKG-INFO → klnote-0.3.0/README.md +105 -100
- klnote-0.3.0/klnote/__init__.py +3 -0
- klnote-0.3.0/klnote/cli.py +284 -0
- klnote-0.3.0/klnote/core/__init__.py +3 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/service.py +71 -2
- klnote-0.3.0/klnote/core/utils/time_1.py +21 -0
- klnote-0.1.0/README.md → klnote-0.3.0/klnote.egg-info/PKG-INFO +116 -90
- {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/requires.txt +1 -0
- {klnote-0.1.0 → klnote-0.3.0}/pyproject.toml +2 -1
- klnote-0.1.0/klnote/cli.py +0 -188
- klnote-0.1.0/klnote/core/__init__.py +0 -0
- klnote-0.1.0/klnote/core/schema/__init__.py +0 -0
- klnote-0.1.0/klnote/core/utils/time_1.py +0 -8
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/constants.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/database.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/exceptions.py +0 -0
- {klnote-0.1.0/klnote → klnote-0.3.0/klnote/core/schema}/__init__.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/schema/schame.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/__init__.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/db.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote/core/utils/editor.py +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/SOURCES.txt +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/dependency_links.txt +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/entry_points.txt +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/klnote.egg-info/top_level.txt +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/setup.cfg +0 -0
- {klnote-0.1.0 → klnote-0.3.0}/tests/test_cli.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
klnote
|
|
38
|
-
klnote
|
|
39
|
-
klnote
|
|
40
|
-
klnote
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
klnote stk
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
|
|
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,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()
|
|
@@ -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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
klnote
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
klnote
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
klnote
|
|
73
|
-
klnote
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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` |
|
|
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "klnote"
|
|
7
|
-
version = "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
|
|
klnote-0.1.0/klnote/cli.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|