klnote 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
klnote/__init__.py ADDED
File without changes
klnote/cli.py ADDED
@@ -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
+
@@ -0,0 +1,5 @@
1
+ class IsNotSqliteDBError(Exception):
2
+ pass
3
+
4
+ class TableExistsError(Exception):
5
+ pass
File without changes
@@ -0,0 +1,3 @@
1
+ NOTES = [
2
+
3
+ ]
klnote/core/service.py ADDED
@@ -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,18 @@
1
+ klnote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ klnote/cli.py,sha256=R5rNVn9wd2jVxmmq3DtX_X9526QRA5Qkuf2AEWMC5wg,5400
3
+ klnote/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ klnote/core/constants.py,sha256=XOaByNCVAAfpYJFMQmOl3_qxjp14pvpfGmgx9B48qqQ,1069
5
+ klnote/core/database.py,sha256=UQ6t-bmb7hvf9HWGF-A8CWTVb6r9Lv5IY_iIgXR7xmI,7011
6
+ klnote/core/exceptions.py,sha256=uK5yIC7BR6X0qD7aTtC5bq5pxMSanEYwlymv3U_QgNg,94
7
+ klnote/core/service.py,sha256=YCZp3Kv0xKyaRYcwKGBpFQm0T1r8DTWFddar_PSPyHs,3365
8
+ klnote/core/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ klnote/core/schema/schame.py,sha256=n7cxYwkPIXuKiwDjCyN-oKrvQWso16kJDiRYOGfsJCQ,14
10
+ klnote/core/utils/__init__.py,sha256=gIeA9Zk4ILW7tMNDuM6IwI48gQuikxcrwqemNtJ9dfs,282
11
+ klnote/core/utils/db.py,sha256=G0ZwwEV_jXU_7fYQA6VI4HPomDDE-165yxG4fXbrcC0,1219
12
+ klnote/core/utils/editor.py,sha256=GSno5xfyqkkVYXpY3zdwUikKR-iH139k9Y7zORqRtNE,1211
13
+ klnote/core/utils/time_1.py,sha256=XeU5zc5BbmzkgR0Rtix5s3G-ip-9DqV1eIrrMa44kLY,362
14
+ klnote-0.1.0.dist-info/METADATA,sha256=Mo8T6twGLpXIKNN7YmYfshg5OsvDWjrJCX-rcVdrNDY,2942
15
+ klnote-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ klnote-0.1.0.dist-info/entry_points.txt,sha256=ZQeROuTN7JHTkjLZK4DahKgInVXQ2g8STkNdPnGWoaA,42
17
+ klnote-0.1.0.dist-info/top_level.txt,sha256=L9Kq96iUm5lauzY8XjpaTI6AgN5sL62f5VX3vkI8n3Q,7
18
+ klnote-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ klnote = klnote.cli:cli
@@ -0,0 +1 @@
1
+ klnote