klnote 0.1.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/PKG-INFO +100 -0
- klnote-0.1.0/README.md +90 -0
- klnote-0.1.0/klnote/__init__.py +0 -0
- klnote-0.1.0/klnote/cli.py +188 -0
- klnote-0.1.0/klnote/core/__init__.py +0 -0
- klnote-0.1.0/klnote/core/constants.py +47 -0
- klnote-0.1.0/klnote/core/database.py +262 -0
- klnote-0.1.0/klnote/core/exceptions.py +5 -0
- klnote-0.1.0/klnote/core/schema/__init__.py +0 -0
- klnote-0.1.0/klnote/core/schema/schame.py +3 -0
- klnote-0.1.0/klnote/core/service.py +104 -0
- klnote-0.1.0/klnote/core/utils/__init__.py +14 -0
- klnote-0.1.0/klnote/core/utils/db.py +38 -0
- klnote-0.1.0/klnote/core/utils/editor.py +41 -0
- klnote-0.1.0/klnote/core/utils/time_1.py +8 -0
- klnote-0.1.0/klnote.egg-info/PKG-INFO +100 -0
- klnote-0.1.0/klnote.egg-info/SOURCES.txt +23 -0
- klnote-0.1.0/klnote.egg-info/dependency_links.txt +1 -0
- klnote-0.1.0/klnote.egg-info/entry_points.txt +2 -0
- klnote-0.1.0/klnote.egg-info/requires.txt +4 -0
- klnote-0.1.0/klnote.egg-info/top_level.txt +1 -0
- klnote-0.1.0/pyproject.toml +25 -0
- klnote-0.1.0/setup.cfg +4 -0
- klnote-0.1.0/tests/test_cli.py +20 -0
- klnote-0.1.0/tests/test_service.py +100 -0
klnote-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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` |
|
klnote-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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` |
|
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
DEFAULT_METADATA_JSON = """{
|
|
4
|
+
"code":null,
|
|
5
|
+
"name":null,
|
|
6
|
+
"country":null,
|
|
7
|
+
"label":null,
|
|
8
|
+
"extra":null
|
|
9
|
+
}"""
|
|
10
|
+
DEFAULT_METADATA = {
|
|
11
|
+
"code":None,
|
|
12
|
+
"name":None,
|
|
13
|
+
"country":None,
|
|
14
|
+
"label":None,
|
|
15
|
+
"extra":None
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def _get_db_path(debug_path:str=None):
|
|
19
|
+
"""获取默认数据库路径,优先级: debug_path>/ KLNOTE_DB_PATH > ~/.KLNote/KLNote.db"""
|
|
20
|
+
if debug_path:
|
|
21
|
+
return os.path.abspath(debug_path)
|
|
22
|
+
env_path = os.environ.get("KLNOTE_DB_PATH")
|
|
23
|
+
if env_path:
|
|
24
|
+
return env_path
|
|
25
|
+
home = os.path.expanduser("~")
|
|
26
|
+
db_dir = os.path.join(home, ".KLNote")
|
|
27
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
28
|
+
return os.path.join(db_dir, "KLNote.db")
|
|
29
|
+
|
|
30
|
+
DB_PATH = _get_db_path()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_TABLE_NAME = "klnote"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NO_WAIT_EDITORS = {
|
|
37
|
+
"notepad", # Windows 记事本
|
|
38
|
+
"vim", "vi", # 终端 vim
|
|
39
|
+
"nano", # 终端 nano
|
|
40
|
+
"gedit", # Linux 编辑器
|
|
41
|
+
"textedit", # macOS 编辑器
|
|
42
|
+
"edit", # Windows 简易编辑
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
ADD_NOTE_INIT_TEXT = ""
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .constants import DEFAULT_METADATA,DB_PATH,DEFAULT_TABLE_NAME
|
|
7
|
+
|
|
8
|
+
from .utils import get_iso_timestamp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Database:
|
|
17
|
+
def __init__(self, path:str, table_name:str=DEFAULT_TABLE_NAME):
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
self.path = path
|
|
21
|
+
self.table_name = table_name
|
|
22
|
+
self.init_db()
|
|
23
|
+
|
|
24
|
+
self.conn = sqlite3.connect(path)
|
|
25
|
+
self.cursor = self.conn.cursor()
|
|
26
|
+
self.cursor.row_factory = sqlite3.Row
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_db(self):
|
|
31
|
+
|
|
32
|
+
conn = sqlite3.connect(self.path)
|
|
33
|
+
cursor = conn.cursor()
|
|
34
|
+
cursor.execute(f'''
|
|
35
|
+
CREATE TABLE IF NOT EXISTS {self.table_name} (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
code TEXT NOT NULL,
|
|
38
|
+
name TEXT,
|
|
39
|
+
metadata TEXT NOT NULL,
|
|
40
|
+
description TEXT,
|
|
41
|
+
notes TEXT,
|
|
42
|
+
created_at TIMESTAMP NOT NULL,
|
|
43
|
+
updated_at TIMESTAMP
|
|
44
|
+
)
|
|
45
|
+
''')
|
|
46
|
+
conn.close()
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def create_new_stock_row(
|
|
50
|
+
|
|
51
|
+
self,
|
|
52
|
+
code: str,
|
|
53
|
+
id: str = None,
|
|
54
|
+
name: str = None,
|
|
55
|
+
metadata: dict = DEFAULT_METADATA,
|
|
56
|
+
description: str = None,
|
|
57
|
+
) -> dict:
|
|
58
|
+
"""
|
|
59
|
+
:param code: 股票编号
|
|
60
|
+
:param id: uuid4().hex
|
|
61
|
+
:param name: 股票名称
|
|
62
|
+
:param metadata: ..constants.METADATA_DEFAULT_JSON
|
|
63
|
+
:param description: 对这只股票的简述
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
id = uuid4().hex if id is None else id
|
|
67
|
+
metadata = json.dumps(metadata)
|
|
68
|
+
notes = "[]"
|
|
69
|
+
created_at = get_iso_timestamp()
|
|
70
|
+
updated_at = None
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"id": id,
|
|
74
|
+
"code": code,
|
|
75
|
+
"name": name,
|
|
76
|
+
"metadata": metadata,
|
|
77
|
+
"description": description,
|
|
78
|
+
"notes": notes,
|
|
79
|
+
"created_at": created_at,
|
|
80
|
+
"updated_at": updated_at,
|
|
81
|
+
}
|
|
82
|
+
def add_new_stock_row(
|
|
83
|
+
self,
|
|
84
|
+
code: str,
|
|
85
|
+
id: str,
|
|
86
|
+
name: str = None,
|
|
87
|
+
metadata: str = None,
|
|
88
|
+
description: str = None,
|
|
89
|
+
notes: str = None,
|
|
90
|
+
created_at: str = None,
|
|
91
|
+
updated_at: str = None,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
:param kwargs: 为 create_new_stock_row 返回值
|
|
96
|
+
:return: self
|
|
97
|
+
"""
|
|
98
|
+
kwargs = {
|
|
99
|
+
"id": id,
|
|
100
|
+
"code": code,
|
|
101
|
+
"name": name,
|
|
102
|
+
"metadata": metadata,
|
|
103
|
+
"description": description,
|
|
104
|
+
"notes": notes,
|
|
105
|
+
"created_at": created_at,
|
|
106
|
+
"updated_at": updated_at,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
self.cursor.execute(
|
|
110
|
+
f'''INSERT INTO {self.table_name} (id, code, name, metadata, description, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
|
|
111
|
+
(kwargs["id"], kwargs["code"], kwargs["name"], kwargs["metadata"], kwargs["description"], kwargs["notes"], kwargs["created_at"], kwargs["updated_at"]))
|
|
112
|
+
self.conn.commit()
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def create_and_add_new_stock_row(
|
|
116
|
+
self,
|
|
117
|
+
code: str,
|
|
118
|
+
id: str = None,
|
|
119
|
+
name: str = None,
|
|
120
|
+
metadata: dict = DEFAULT_METADATA,
|
|
121
|
+
description: str = None,
|
|
122
|
+
):
|
|
123
|
+
kwargs = self.create_new_stock_row(code, id, name, metadata, description)
|
|
124
|
+
self.add_new_stock_row(**kwargs)
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def read(self,ids: str|list[str]="*", fields_for_reading: str|list[str]="*") -> dict[dict[str, str]]:
|
|
129
|
+
"""从数据库读取信息 ids、fields 但字段查询为字段名str,多字段查询为list,元素是字段名
|
|
130
|
+
:param ids 查询的row的id
|
|
131
|
+
:param fields 查询 需返回的字段名
|
|
132
|
+
:return
|
|
133
|
+
若 有结果, 返回字典:{"id":{"字段":"值"}}
|
|
134
|
+
若没有结果, 返回 {}
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
result_map = {}
|
|
138
|
+
|
|
139
|
+
# 把列表转化成 sql 可用的字符串
|
|
140
|
+
|
|
141
|
+
if ids == "*":
|
|
142
|
+
self.cursor.execute(f"""SELECT id FROM {self.table_name}""")
|
|
143
|
+
rows = list(self.cursor.fetchall())
|
|
144
|
+
ids_list = []
|
|
145
|
+
for row in rows:
|
|
146
|
+
ids_list.append(row["id"])
|
|
147
|
+
ids = ids_list
|
|
148
|
+
elif isinstance(ids, str):
|
|
149
|
+
ids = [ids]
|
|
150
|
+
elif isinstance(ids, list):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
for id in ids:
|
|
154
|
+
result = self.read_one(id, fields_for_reading)
|
|
155
|
+
result_map[id] = result
|
|
156
|
+
|
|
157
|
+
return result_map
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def read_one(self,id:str, read_fields: str|list[str]="*"):
|
|
162
|
+
|
|
163
|
+
self.cursor.execute(f'''
|
|
164
|
+
SELECT {read_fields} FROM {self.table_name}
|
|
165
|
+
WHERE id = ?
|
|
166
|
+
''', (id,))
|
|
167
|
+
row = self.cursor.fetchone()
|
|
168
|
+
if row is None:
|
|
169
|
+
return {}
|
|
170
|
+
row = dict(row)
|
|
171
|
+
|
|
172
|
+
if row.get("metadata"):
|
|
173
|
+
row["metadata"] = json.loads(row["metadata"])
|
|
174
|
+
if row.get("notes"):
|
|
175
|
+
row["notes"] = json.loads(row["notes"])
|
|
176
|
+
result = row
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def update(self,id: str,field_for_updating:str, content: str):
|
|
185
|
+
"""覆写数据库
|
|
186
|
+
:param id 覆写 位置 所在 列的 id
|
|
187
|
+
:param field 覆写 的字段
|
|
188
|
+
:param content 覆写的内容
|
|
189
|
+
"""
|
|
190
|
+
self.cursor.execute(f"""
|
|
191
|
+
UPDATE {self.table_name} SET {field_for_updating}=? WHERE id = ?
|
|
192
|
+
""",(content,id))
|
|
193
|
+
self.conn.commit()
|
|
194
|
+
|
|
195
|
+
def delete(self,id: str):
|
|
196
|
+
self.cursor.execute(f"""
|
|
197
|
+
DELETE FROM {self.table_name} WHERE id = ?
|
|
198
|
+
""",(id,))
|
|
199
|
+
self.conn.commit()
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def match(self, value,field_of_value: str ):
|
|
204
|
+
"""匹配符合条件的 id
|
|
205
|
+
:return: list[id]"""
|
|
206
|
+
self.cursor.execute(f"""
|
|
207
|
+
SELECT id FROM {self.table_name} WHERE {field_of_value} = ?""",(value,))
|
|
208
|
+
ids = []
|
|
209
|
+
for row in list(self.cursor.fetchall()):
|
|
210
|
+
ids.append(row["id"])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
return ids
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def match_and_read(self, value ,field_of_value: str ):
|
|
220
|
+
|
|
221
|
+
result_map = {}
|
|
222
|
+
ids = self.match(value,field_of_value)
|
|
223
|
+
for id in ids:
|
|
224
|
+
a_result_map = self.read(id)
|
|
225
|
+
result_map.update(a_result_map)
|
|
226
|
+
return result_map
|
|
227
|
+
|
|
228
|
+
def match_and_update(self, value ,field_of_value: str, field_for_updating: str ):
|
|
229
|
+
|
|
230
|
+
ids = self.match(value,field_of_value)
|
|
231
|
+
for id in ids:
|
|
232
|
+
self.update(id,field_for_updating)
|
|
233
|
+
return self
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
db = Database(DB_PATH)
|
|
242
|
+
#
|
|
243
|
+
db.create_and_add_new_stock_row(
|
|
244
|
+
code="000001",id="1" )
|
|
245
|
+
|
|
246
|
+
# rows = db.read(ids="1",fields="code")
|
|
247
|
+
print(db.read())
|
|
248
|
+
|
|
249
|
+
# print(rows)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .database import Database
|
|
4
|
+
from .constants import DB_PATH
|
|
5
|
+
from .utils import get_iso_timestamp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Service:
|
|
9
|
+
def __init__(self, db: Database = None):
|
|
10
|
+
self.db = db or Database(DB_PATH)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def search(self,id:str=None, code:str=None, name:str=None):
|
|
14
|
+
"""
|
|
15
|
+
id code name 三选一 查询,若多填, 优先 找 id 其次 code 最后 name
|
|
16
|
+
|
|
17
|
+
:return ids: list[id]
|
|
18
|
+
"""
|
|
19
|
+
if id:
|
|
20
|
+
ids = self.db.match(id, "id")
|
|
21
|
+
elif code:
|
|
22
|
+
ids = self.db.match(code,"code" )
|
|
23
|
+
elif name:
|
|
24
|
+
ids = self.db.match(name, "name" )
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
return ids
|
|
28
|
+
|
|
29
|
+
# stock
|
|
30
|
+
def stock_add(self, code: str, id: str = None, name: str = None, description: str = None):
|
|
31
|
+
"""新建股票条目"""
|
|
32
|
+
self.db.create_and_add_new_stock_row(code=code, id=id, name=name, description=description)
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def stock_delete(self, id: str):
|
|
36
|
+
"""删除股票"""
|
|
37
|
+
self.db.delete(id)
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def stock_show(self, id: str):
|
|
41
|
+
"""展示单支股票完整信息"""
|
|
42
|
+
result = self.db.read(id)
|
|
43
|
+
return result.get(id, {})
|
|
44
|
+
|
|
45
|
+
def stock_list(self):
|
|
46
|
+
"""列出所有股票"""
|
|
47
|
+
return list(self.db.read().values())
|
|
48
|
+
|
|
49
|
+
def _build_note(self, content: str, summary: str = None, extra: dict = None) -> dict:
|
|
50
|
+
"""构建标准笔记结构"""
|
|
51
|
+
now = get_iso_timestamp()
|
|
52
|
+
return {
|
|
53
|
+
"summary": summary,
|
|
54
|
+
"content": content,
|
|
55
|
+
"created_at": now,
|
|
56
|
+
"updated_at": None,
|
|
57
|
+
"extra": extra or {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def note_add(self, stock_id: str, content: str, summary: str = None, extra: dict = None):
|
|
61
|
+
"""给股票添加一条笔记"""
|
|
62
|
+
stock = self.stock_show(stock_id)
|
|
63
|
+
notes = stock.get("notes", [])
|
|
64
|
+
note = self._build_note(content, summary, extra)
|
|
65
|
+
notes.append(note)
|
|
66
|
+
self.db.update(stock_id, "notes", json.dumps(notes))
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def note_delete(self, stock_id: str, index: int):
|
|
70
|
+
"""删除股票指定索引的笔记"""
|
|
71
|
+
stock = self.stock_show(stock_id)
|
|
72
|
+
notes = stock.get("notes", [])
|
|
73
|
+
notes.pop(index)
|
|
74
|
+
self.db.update(stock_id, "notes", json.dumps(notes))
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def _read_notes(self, stock_id: str) -> list:
|
|
78
|
+
"""读取股票所有笔记(内部用)"""
|
|
79
|
+
stock = self.stock_show(stock_id)
|
|
80
|
+
return stock.get("notes", [])
|
|
81
|
+
|
|
82
|
+
def note_read(self, stock_id: str, indices: list[int] = None):
|
|
83
|
+
"""读取股票笔记,默认读全部,可指定索引列表"""
|
|
84
|
+
note_list = self._read_notes(stock_id)
|
|
85
|
+
if indices is None:
|
|
86
|
+
return note_list
|
|
87
|
+
return [note_list[i] for i in indices if 0 <= i < len(note_list)]
|
|
88
|
+
|
|
89
|
+
# admin
|
|
90
|
+
def match_and_read(self, value, field_of_value: str):
|
|
91
|
+
"""按字段匹配并读取"""
|
|
92
|
+
return self.db.match_and_read(value, field_of_value)
|
|
93
|
+
|
|
94
|
+
def match_and_update(self, value, field_of_value: str, field_for_updating: str):
|
|
95
|
+
"""按字段匹配并更新"""
|
|
96
|
+
self.db.match_and_update(value, field_of_value, field_for_updating)
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
service = Service()
|
|
102
|
+
# 测试
|
|
103
|
+
service.stock_add(code="000001", name="平安银行", description="银行股")
|
|
104
|
+
print(service.stock_list())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .db import is_sqlite_db, table_exists, check_file_and_table
|
|
2
|
+
from .time_1 import get_iso_timestamp
|
|
3
|
+
from .editor import edit_in_editor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"is_sqlite_db",
|
|
9
|
+
"table_exists",
|
|
10
|
+
"check_file_and_table",
|
|
11
|
+
'get_iso_timestamp',
|
|
12
|
+
'edit_in_editor'
|
|
13
|
+
|
|
14
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from ..exceptions import IsNotSqliteDBError, TableExistsError
|
|
4
|
+
|
|
5
|
+
def is_sqlite_db(file_path):
|
|
6
|
+
"""判断文件是否为 SQLite 数据库文件
|
|
7
|
+
:param file_path:
|
|
8
|
+
return:若是,则返回Ture,若不是则返回
|
|
9
|
+
"""
|
|
10
|
+
if not os.path.isfile(file_path):
|
|
11
|
+
raise FileExistsError(f"文件不存在:{file_path}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def table_exists(path:str, table_name:str) -> bool:
|
|
15
|
+
"""判断 文件内是否存在指定表名, 存在-> Ture 不存在-> False"""
|
|
16
|
+
import sqlite3
|
|
17
|
+
conn = sqlite3.connect(path)
|
|
18
|
+
cursor = conn.cursor()
|
|
19
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type=table AND name=?", (table_name,))
|
|
20
|
+
return cursor.fetchone()
|
|
21
|
+
|
|
22
|
+
def check_file_and_table(self, path: str, table_name: str = "kl-note"):
|
|
23
|
+
# 确保db文件存在
|
|
24
|
+
if not is_sqlite_db(path):
|
|
25
|
+
raise FileExistsError(f"文件 '{path}' 不存在")
|
|
26
|
+
|
|
27
|
+
# 检查是否是sql文件
|
|
28
|
+
if not is_sqlite_db(path):
|
|
29
|
+
raise IsNotSqliteDBError(f"'{path}' 不是 sql数据库 文件")
|
|
30
|
+
|
|
31
|
+
# 检查 是否存在 指定表
|
|
32
|
+
if not table_exists(path, table_name):
|
|
33
|
+
raise TableExistsError(f"文件 '{path}' 内 不存在 表 '{table_name}'")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import scrolledtext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def edit_in_editor(initial_content: str) -> str | None:
|
|
8
|
+
"""内置编辑器,返回编辑后内容。用户点 X 视为取消,返回 None"""
|
|
9
|
+
|
|
10
|
+
result = [None] # 闭包用
|
|
11
|
+
|
|
12
|
+
def on_save():
|
|
13
|
+
result[0] = text_area.get('1.0', 'end-1c')
|
|
14
|
+
window.destroy()
|
|
15
|
+
|
|
16
|
+
def on_cancel():
|
|
17
|
+
window.destroy()
|
|
18
|
+
|
|
19
|
+
window = tk.Tk()
|
|
20
|
+
window.title("编辑器 - KLNote")
|
|
21
|
+
|
|
22
|
+
text_area = scrolledtext.ScrolledText(window, width=80, height=24, font=(' Consolas ', 10))
|
|
23
|
+
text_area.insert('1.0', initial_content)
|
|
24
|
+
text_area.pack(fill='both', expand=True)
|
|
25
|
+
|
|
26
|
+
btn_frame = tk.Frame(window)
|
|
27
|
+
btn_frame.pack(fill='x')
|
|
28
|
+
tk.Button(btn_frame, text="保存 (Ctrl+S)", command=on_save).pack(side='left', padx=5, pady=5)
|
|
29
|
+
tk.Button(btn_frame, text="取消", command=on_cancel).pack(side='right', padx=5, pady=5)
|
|
30
|
+
|
|
31
|
+
window.bind('<Control-s>', lambda e: on_save()) # Ctrl+S 保存
|
|
32
|
+
window.protocol("WM_DELETE_WINDOW", on_cancel) # 点 X 视为取消
|
|
33
|
+
window.mainloop()
|
|
34
|
+
|
|
35
|
+
return result[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
cont = edit_in_editor(initial_content="你好")
|
|
41
|
+
print(cont)
|
|
@@ -0,0 +1,8 @@
|
|
|
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')
|
|
@@ -0,0 +1,100 @@
|
|
|
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` |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
klnote/__init__.py
|
|
4
|
+
klnote/cli.py
|
|
5
|
+
klnote.egg-info/PKG-INFO
|
|
6
|
+
klnote.egg-info/SOURCES.txt
|
|
7
|
+
klnote.egg-info/dependency_links.txt
|
|
8
|
+
klnote.egg-info/entry_points.txt
|
|
9
|
+
klnote.egg-info/requires.txt
|
|
10
|
+
klnote.egg-info/top_level.txt
|
|
11
|
+
klnote/core/__init__.py
|
|
12
|
+
klnote/core/constants.py
|
|
13
|
+
klnote/core/database.py
|
|
14
|
+
klnote/core/exceptions.py
|
|
15
|
+
klnote/core/service.py
|
|
16
|
+
klnote/core/schema/__init__.py
|
|
17
|
+
klnote/core/schema/schame.py
|
|
18
|
+
klnote/core/utils/__init__.py
|
|
19
|
+
klnote/core/utils/db.py
|
|
20
|
+
klnote/core/utils/editor.py
|
|
21
|
+
klnote/core/utils/time_1.py
|
|
22
|
+
tests/test_cli.py
|
|
23
|
+
tests/test_service.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
klnote
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "klnote"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Stock note management tool"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click>=8.0.0",
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.0.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
klnote = "klnote.cli:cli"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["."]
|
|
25
|
+
include = ["klnote*"]
|
klnote-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from click.testing import CliRunner
|
|
3
|
+
from klnote.cli import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def runner():
|
|
8
|
+
return CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_cli_help(runner):
|
|
12
|
+
result = runner.invoke(cli, ["--help"])
|
|
13
|
+
assert result.exit_code == 0
|
|
14
|
+
assert "KLNote" in result.output
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_stock_ls_empty(runner):
|
|
18
|
+
result = runner.invoke(cli, ["stk", "ls"])
|
|
19
|
+
assert result.exit_code == 0
|
|
20
|
+
assert "暂无股票" in result.output
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from klnote.core.database import Database
|
|
5
|
+
from klnote.core.service import Service
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def tmp_db():
|
|
10
|
+
path = tempfile.mktemp(suffix=".db")
|
|
11
|
+
db = Database(path)
|
|
12
|
+
yield db
|
|
13
|
+
db.conn.close()
|
|
14
|
+
try:
|
|
15
|
+
os.unlink(path)
|
|
16
|
+
except OSError:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def service(tmp_db):
|
|
22
|
+
return Service(db=tmp_db)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestStock:
|
|
26
|
+
def test_add(self, service):
|
|
27
|
+
service.stock_add(code="000001", name="平安银行")
|
|
28
|
+
stocks = service.stock_list()
|
|
29
|
+
assert len(stocks) == 1
|
|
30
|
+
assert stocks[0]["code"] == "000001"
|
|
31
|
+
assert stocks[0]["name"] == "平安银行"
|
|
32
|
+
|
|
33
|
+
def test_list_empty(self, service):
|
|
34
|
+
assert service.stock_list() == []
|
|
35
|
+
|
|
36
|
+
def test_show(self, service):
|
|
37
|
+
service.stock_add(code="000001", name="平安银行")
|
|
38
|
+
stock = service.stock_show(service.stock_list()[0]["id"])
|
|
39
|
+
assert stock["code"] == "000001"
|
|
40
|
+
|
|
41
|
+
def test_delete(self, service):
|
|
42
|
+
service.stock_add(code="000001")
|
|
43
|
+
stocks = service.stock_list()
|
|
44
|
+
service.stock_delete(stocks[0]["id"])
|
|
45
|
+
assert service.stock_list() == []
|
|
46
|
+
|
|
47
|
+
def test_search_by_code(self, service):
|
|
48
|
+
service.stock_add(code="000001", name="平安银行")
|
|
49
|
+
ids = service.search(code="000001")
|
|
50
|
+
assert len(ids) == 1
|
|
51
|
+
|
|
52
|
+
def test_search_by_name(self, service):
|
|
53
|
+
service.stock_add(code="000001", name="平安银行")
|
|
54
|
+
ids = service.search(name="平安银行")
|
|
55
|
+
assert len(ids) == 1
|
|
56
|
+
|
|
57
|
+
def test_search_by_id(self, service):
|
|
58
|
+
service.stock_add(code="000001")
|
|
59
|
+
stock_id = service.stock_list()[0]["id"]
|
|
60
|
+
ids = service.search(id=stock_id)
|
|
61
|
+
assert len(ids) == 1
|
|
62
|
+
assert ids[0] == stock_id
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestNote:
|
|
66
|
+
def test_add(self, service):
|
|
67
|
+
service.stock_add(code="000001")
|
|
68
|
+
stock_id = service.stock_list()[0]["id"]
|
|
69
|
+
service.note_add(stock_id, content="测试内容", summary="测试摘要")
|
|
70
|
+
notes = service.note_read(stock_id)
|
|
71
|
+
assert len(notes) == 1
|
|
72
|
+
assert notes[0]["content"] == "测试内容"
|
|
73
|
+
assert notes[0]["summary"] == "测试摘要"
|
|
74
|
+
|
|
75
|
+
def test_delete(self, service):
|
|
76
|
+
service.stock_add(code="000001")
|
|
77
|
+
stock_id = service.stock_list()[0]["id"]
|
|
78
|
+
service.note_add(stock_id, content="内容1")
|
|
79
|
+
service.note_add(stock_id, content="内容2")
|
|
80
|
+
service.note_delete(stock_id, 0)
|
|
81
|
+
notes = service.note_read(stock_id)
|
|
82
|
+
assert len(notes) == 1
|
|
83
|
+
assert notes[0]["content"] == "内容2"
|
|
84
|
+
|
|
85
|
+
def test_read_empty(self, service):
|
|
86
|
+
service.stock_add(code="000001")
|
|
87
|
+
stock_id = service.stock_list()[0]["id"]
|
|
88
|
+
notes = service.note_read(stock_id)
|
|
89
|
+
assert notes == []
|
|
90
|
+
|
|
91
|
+
def test_read_by_indices(self, service):
|
|
92
|
+
service.stock_add(code="000001")
|
|
93
|
+
stock_id = service.stock_list()[0]["id"]
|
|
94
|
+
service.note_add(stock_id, content="0")
|
|
95
|
+
service.note_add(stock_id, content="1")
|
|
96
|
+
service.note_add(stock_id, content="2")
|
|
97
|
+
notes = service.note_read(stock_id, indices=[0, 2])
|
|
98
|
+
assert len(notes) == 2
|
|
99
|
+
assert notes[0]["content"] == "0"
|
|
100
|
+
assert notes[1]["content"] == "2"
|