daochu 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.
daochu-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DaoChu Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
daochu-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: daochu
3
+ Version: 0.1.0
4
+ Summary: 导出微信聊天记录为 HTML / Markdown — Export WeChat conversations to HTML or Markdown
5
+ Author-email: DaoChu Contributors <daochu@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/daochu/daochu
8
+ Project-URL: Repository, https://github.com/daochu/daochu
9
+ Keywords: wechat,export,chat,html,markdown,backup
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Communications :: Chat
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: click>=8.0
23
+ Requires-Dist: jinja2>=3.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.0; extra == "dev"
26
+ Requires-Dist: twine>=5.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # DaoChu(导出)
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/daochu.svg)](https://pypi.org/project/daochu/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/daochu.svg)](https://pypi.org/project/daochu/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ **DaoChu**(导出)是一个 Python 命令行工具,能将微信聊天记录导出为精美的 **HTML** 页面或 **Markdown** 文件。
36
+
37
+ > 支持解密后的微信 SQLite 数据库、CSV 导出、JSON 导出三种数据源。
38
+
39
+ ---
40
+
41
+ ## ✨ 特性
42
+
43
+ - 🎨 **精美 HTML 输出** — 仿微信气泡样式,带日期分组、响应式设计
44
+ - 📝 **Markdown 输出** — 纯文本,适合归档或导入笔记工具
45
+ - 🔍 **自动联系人发现** — `list-contacts` 命令快速查看所有联系人
46
+ - 🧠 **智能解析** — 自动识别文件类型(.db / .csv / .json)
47
+ - 🖼️ **媒体标注** — 图片、语音、视频、链接等自动标注为可读文本
48
+
49
+ ---
50
+
51
+ ## 📦 安装
52
+
53
+ ```bash
54
+ pip install daochu
55
+ ```
56
+
57
+ 要求 Python ≥ 3.10。
58
+
59
+ ---
60
+
61
+ ## 🚀 快速开始
62
+
63
+ ### 1. 列出联系人
64
+
65
+ ```bash
66
+ daochu list-contacts /path/to/decrypted/MSG.db
67
+ ```
68
+
69
+ ### 2. 导出聊天记录
70
+
71
+ ```bash
72
+ # 导出为 HTML(默认)
73
+ daochu export MSG.db "wxid_abc123"
74
+
75
+ # 导出为 Markdown
76
+ daochu export MSG.db "wxid_abc123" -f md
77
+
78
+ # 指定输出路径
79
+ daochu export MSG.db "wxid_abc123" -o ~/Desktop/chat.html
80
+
81
+ # 自定义自己的显示名
82
+ daochu export MSG.db "wxid_abc123" --me "小明"
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 📂 数据源说明
88
+
89
+ | 类型 | 说明 |
90
+ |------|------|
91
+ | **SQLite (.db)** | 解密后的微信 Msg 数据库,最常见的方式 |
92
+ | **CSV (.csv)** | 从第三方工具导出的 CSV,需含 timestamp/sender/content 列 |
93
+ | **JSON (.json)** | 消息对象数组,格式 `[{timestamp, sender, content}]` |
94
+
95
+ > **如何获取解密数据库?** 微信数据库默认加密存储在 `Documents/WeChat Files/<wxid>/Msg/` 下。可使用开源工具解密后配合 DaoChu 使用。
96
+
97
+ ---
98
+
99
+ ## 🖥️ 命令行参考
100
+
101
+ ```
102
+ Usage: daochu [OPTIONS] COMMAND [ARGS]...
103
+
104
+ Commands:
105
+ export 导出与某个联系人的全部聊天记录
106
+ list-contacts 列出数据库中的所有联系人
107
+
108
+ export 选项:
109
+ -o, --output PATH 输出目录或文件路径
110
+ -f, --format [html|md] 输出格式
111
+ --me TEXT 自己的显示名称
112
+ --source-type [auto|db|csv|json]
113
+ --table TEXT 数据库表名
114
+ --encoding TEXT 文件编码
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🐍 Python API
120
+
121
+ ```python
122
+ from daochu.parser import parse_db
123
+ from daochu.exporter import export
124
+
125
+ conv = parse_db("MSG0.db", "wxid_abc123", my_name="我")
126
+ export(conv, "output.html", fmt="html")
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 🖼️ 效果预览
132
+
133
+ HTML 输出效果:
134
+
135
+ - 顶部:联系人名称、日期范围、消息总数
136
+ - 每一条消息:微信风格气泡,自己的消息靠右显示为绿色
137
+ - 日期分组:按天自动插入日期分隔线
138
+ - 移动端适配:响应式设计,手机上也能流畅阅读
139
+
140
+ ---
141
+
142
+ ## 📄 License
143
+
144
+ MIT © DaoChu Contributors
daochu-0.1.0/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # DaoChu(导出)
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/daochu.svg)](https://pypi.org/project/daochu/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/daochu.svg)](https://pypi.org/project/daochu/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **DaoChu**(导出)是一个 Python 命令行工具,能将微信聊天记录导出为精美的 **HTML** 页面或 **Markdown** 文件。
8
+
9
+ > 支持解密后的微信 SQLite 数据库、CSV 导出、JSON 导出三种数据源。
10
+
11
+ ---
12
+
13
+ ## ✨ 特性
14
+
15
+ - 🎨 **精美 HTML 输出** — 仿微信气泡样式,带日期分组、响应式设计
16
+ - 📝 **Markdown 输出** — 纯文本,适合归档或导入笔记工具
17
+ - 🔍 **自动联系人发现** — `list-contacts` 命令快速查看所有联系人
18
+ - 🧠 **智能解析** — 自动识别文件类型(.db / .csv / .json)
19
+ - 🖼️ **媒体标注** — 图片、语音、视频、链接等自动标注为可读文本
20
+
21
+ ---
22
+
23
+ ## 📦 安装
24
+
25
+ ```bash
26
+ pip install daochu
27
+ ```
28
+
29
+ 要求 Python ≥ 3.10。
30
+
31
+ ---
32
+
33
+ ## 🚀 快速开始
34
+
35
+ ### 1. 列出联系人
36
+
37
+ ```bash
38
+ daochu list-contacts /path/to/decrypted/MSG.db
39
+ ```
40
+
41
+ ### 2. 导出聊天记录
42
+
43
+ ```bash
44
+ # 导出为 HTML(默认)
45
+ daochu export MSG.db "wxid_abc123"
46
+
47
+ # 导出为 Markdown
48
+ daochu export MSG.db "wxid_abc123" -f md
49
+
50
+ # 指定输出路径
51
+ daochu export MSG.db "wxid_abc123" -o ~/Desktop/chat.html
52
+
53
+ # 自定义自己的显示名
54
+ daochu export MSG.db "wxid_abc123" --me "小明"
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 📂 数据源说明
60
+
61
+ | 类型 | 说明 |
62
+ |------|------|
63
+ | **SQLite (.db)** | 解密后的微信 Msg 数据库,最常见的方式 |
64
+ | **CSV (.csv)** | 从第三方工具导出的 CSV,需含 timestamp/sender/content 列 |
65
+ | **JSON (.json)** | 消息对象数组,格式 `[{timestamp, sender, content}]` |
66
+
67
+ > **如何获取解密数据库?** 微信数据库默认加密存储在 `Documents/WeChat Files/<wxid>/Msg/` 下。可使用开源工具解密后配合 DaoChu 使用。
68
+
69
+ ---
70
+
71
+ ## 🖥️ 命令行参考
72
+
73
+ ```
74
+ Usage: daochu [OPTIONS] COMMAND [ARGS]...
75
+
76
+ Commands:
77
+ export 导出与某个联系人的全部聊天记录
78
+ list-contacts 列出数据库中的所有联系人
79
+
80
+ export 选项:
81
+ -o, --output PATH 输出目录或文件路径
82
+ -f, --format [html|md] 输出格式
83
+ --me TEXT 自己的显示名称
84
+ --source-type [auto|db|csv|json]
85
+ --table TEXT 数据库表名
86
+ --encoding TEXT 文件编码
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🐍 Python API
92
+
93
+ ```python
94
+ from daochu.parser import parse_db
95
+ from daochu.exporter import export
96
+
97
+ conv = parse_db("MSG0.db", "wxid_abc123", my_name="我")
98
+ export(conv, "output.html", fmt="html")
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 🖼️ 效果预览
104
+
105
+ HTML 输出效果:
106
+
107
+ - 顶部:联系人名称、日期范围、消息总数
108
+ - 每一条消息:微信风格气泡,自己的消息靠右显示为绿色
109
+ - 日期分组:按天自动插入日期分隔线
110
+ - 移动端适配:响应式设计,手机上也能流畅阅读
111
+
112
+ ---
113
+
114
+ ## 📄 License
115
+
116
+ MIT © DaoChu Contributors
@@ -0,0 +1,20 @@
1
+ """
2
+ DaoChu (导出) — Export WeChat conversations to HTML or Markdown.
3
+
4
+ Usage:
5
+ daochu export <db_path> <contact> [--format html|md] [-o output]
6
+ """
7
+
8
+ from .exporter import export, render_markdown
9
+ from .parser import parse_csv, parse_db, parse_json
10
+ from .templates import render_html
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = [
14
+ "export",
15
+ "parse_db",
16
+ "parse_csv",
17
+ "parse_json",
18
+ "render_html",
19
+ "render_markdown",
20
+ ]
@@ -0,0 +1,146 @@
1
+ """CLI for DaoChu — 微信聊天记录导出工具."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from . import __version__
11
+ from .exporter import export
12
+ from .parser import auto_parse, parse_csv, parse_db, parse_json
13
+
14
+
15
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
16
+ @click.version_option(__version__, "-V", "--version", message="DaoChu %(version)s")
17
+ def main():
18
+ """DaoChu(导出)— 将微信聊天记录导出为 HTML 或 Markdown。
19
+
20
+ 支持三种数据源:
21
+ \b
22
+ 1. 解密后的微信 SQLite 数据库(MSG.db)
23
+ 2. CSV 导出文件
24
+ 3. JSON 导出文件
25
+ """
26
+
27
+
28
+ @main.command("export")
29
+ @click.argument("source", type=click.Path(exists=True))
30
+ @click.argument("contact")
31
+ @click.option(
32
+ "-o", "--output", default=".",
33
+ type=click.Path(),
34
+ help="输出目录或文件路径(默认当前目录)",
35
+ )
36
+ @click.option(
37
+ "-f", "--format", "fmt",
38
+ type=click.Choice(["html", "md"]),
39
+ default="html",
40
+ help="输出格式 (默认 html)",
41
+ )
42
+ @click.option(
43
+ "--me", "my_name",
44
+ default="Me",
45
+ help="自己的显示名称 (默认 Me)",
46
+ )
47
+ @click.option(
48
+ "--source-type", "source_type",
49
+ type=click.Choice(["auto", "db", "csv", "json"]),
50
+ default="auto",
51
+ help="数据源类型 (默认 auto 自动检测)",
52
+ )
53
+ @click.option(
54
+ "--table",
55
+ default="MSG",
56
+ help="数据库表名 (默认 MSG,仅 db 模式)",
57
+ )
58
+ @click.option(
59
+ "--encoding",
60
+ default="utf-8-sig",
61
+ help="CSV/JSON 文件编码 (默认 utf-8-sig)",
62
+ )
63
+ def export_cmd(
64
+ source, contact, output, fmt, my_name, source_type, table, encoding
65
+ ):
66
+ """导出与某个联系人的全部聊天记录。
67
+
68
+ SOURCE: 数据文件路径(解密后的 .db / .csv / .json)\n
69
+ CONTACT: 联系人 wxid 或显示名
70
+ """
71
+ source_path = Path(source)
72
+
73
+ try:
74
+ if source_type == "auto":
75
+ conv = auto_parse(source_path, contact, my_name, table=table)
76
+ elif source_type == "db":
77
+ conv = parse_db(source_path, contact, my_name, table=table)
78
+ elif source_type == "csv":
79
+ conv = parse_csv(source_path, contact, my_name, encoding=encoding)
80
+ elif source_type == "json":
81
+ conv = parse_json(source_path, contact, my_name, encoding=encoding)
82
+ else:
83
+ click.echo(f"未知数据源类型: {source_type}", err=True)
84
+ sys.exit(1)
85
+ except Exception as e:
86
+ click.echo(f"❌ 解析失败: {e}", err=True)
87
+ sys.exit(1)
88
+
89
+ if not conv.messages:
90
+ click.echo(f"⚠ 未找到与 '{contact}' 的聊天记录。")
91
+ sys.exit(0)
92
+
93
+ export(conv, output, fmt)
94
+
95
+
96
+ @main.command("list-contacts")
97
+ @click.argument("source", type=click.Path(exists=True))
98
+ def list_contacts(source):
99
+ """列出数据库中的所有联系人。
100
+
101
+ SOURCE: 解密后的微信 SQLite 数据库路径。
102
+ """
103
+ import sqlite3
104
+
105
+ conn = sqlite3.connect(str(source))
106
+ cur = conn.cursor()
107
+
108
+ # Find MSG table
109
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
110
+ tables = [r[0] for r in cur.fetchall()]
111
+ msg_tables = [t for t in tables if "MSG" in t.upper()]
112
+
113
+ if not msg_tables:
114
+ click.echo("❌ 未找到消息表。", err=True)
115
+ conn.close()
116
+ sys.exit(1)
117
+
118
+ table = msg_tables[0]
119
+
120
+ # Try common contact column names
121
+ for col in ("StrTalker", "talker", "sender", "contact"):
122
+ try:
123
+ cur.execute(
124
+ f'SELECT DISTINCT {col}, COUNT(*) as cnt FROM "{table}" '
125
+ f"GROUP BY {col} ORDER BY cnt DESC"
126
+ )
127
+ rows = cur.fetchall()
128
+ if rows:
129
+ click.echo(f"\n📋 联系人列表 (来自 {table}.{col}):\n")
130
+ click.echo(f"{'联系人 wxid/名称':<40} {'消息数':>8}")
131
+ click.echo("-" * 52)
132
+ for name, cnt in rows:
133
+ name_str = str(name)[:38]
134
+ click.echo(f"{name_str:<40} {cnt:>8}")
135
+ click.echo("")
136
+ break
137
+ except sqlite3.OperationalError:
138
+ continue
139
+ else:
140
+ click.echo("❌ 无法识别联系人列。请手动指定。", err=True)
141
+
142
+ conn.close()
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,94 @@
1
+ """Export a Conversation to HTML or Markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from .models import Conversation
9
+ from .templates import render_html
10
+
11
+
12
+ def render_markdown(conv: Conversation) -> str:
13
+ """Render a Conversation to Markdown."""
14
+ lines: list[str] = []
15
+ lines.append(f"# 💬 与 {conv.contact_name} 的微信聊天记录")
16
+ lines.append("")
17
+
18
+ if conv.messages:
19
+ first = conv.messages[0].date_str
20
+ last = conv.messages[-1].date_str
21
+ lines.append(f"> 📅 {first} — {last} · 📝 {conv.message_count} 条消息")
22
+ lines.append("")
23
+ lines.append("---")
24
+ lines.append("")
25
+
26
+ for date, msgs in conv.group_by_date():
27
+ lines.append(f"### {date}")
28
+ lines.append("")
29
+ for m in msgs:
30
+ role = conv.my_name if m.is_self else m.sender_name
31
+ time_str = m.short_time
32
+ content = m.content.replace("\n", "\n> ")
33
+ lines.append(f"**{role}** _{time_str}_")
34
+ lines.append(f"> {content}")
35
+ lines.append("")
36
+ lines.append("")
37
+
38
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
39
+ lines.append("---")
40
+ lines.append(f"*由 [DaoChu](https://pypi.org/project/daochu/) 导出 · {now}*")
41
+
42
+ return "\n".join(lines)
43
+
44
+
45
+ def export(
46
+ conv: Conversation,
47
+ output_path: str | Path,
48
+ fmt: str = "html",
49
+ ) -> Path:
50
+ """
51
+ Export a conversation to a file.
52
+
53
+ Parameters
54
+ ----------
55
+ conv : Conversation
56
+ The parsed conversation.
57
+ output_path : str | Path
58
+ Destination file path (auto-suffixed if it's a directory).
59
+ fmt : str
60
+ ``"html"`` or ``"md"`` / ``"markdown"``.
61
+
62
+ Returns
63
+ -------
64
+ Path
65
+ The path of the written file.
66
+ """
67
+ output_path = Path(output_path)
68
+
69
+ if output_path.is_dir() or output_path.suffix not in (".html", ".md", ".markdown"):
70
+ safe_name = _safe_filename(conv.contact_name)
71
+ suffix = ".html" if fmt == "html" else ".md"
72
+ output_path = output_path / f"{safe_name}_chat{suffix}"
73
+
74
+ output_path.parent.mkdir(parents=True, exist_ok=True)
75
+
76
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
77
+
78
+ if fmt in ("md", "markdown"):
79
+ content = render_markdown(conv)
80
+ else:
81
+ content = render_html(conv, now_str)
82
+
83
+ output_path.write_text(content, encoding="utf-8")
84
+ print(f"✅ 导出成功 → {output_path} ({conv.message_count} 条消息)")
85
+ return output_path
86
+
87
+
88
+ def _safe_filename(name: str) -> str:
89
+ """Sanitize a name for use as a filename."""
90
+ unsafe = r'<>:"/\|?*'
91
+ safe = name
92
+ for ch in unsafe:
93
+ safe = safe.replace(ch, "_")
94
+ return safe.strip()
@@ -0,0 +1,61 @@
1
+ """Data models for WeChat messages and conversations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Optional
8
+
9
+
10
+ @dataclass
11
+ class Message:
12
+ """A single WeChat message."""
13
+
14
+ id: int
15
+ timestamp: datetime
16
+ sender_name: str # who sent it
17
+ content: str # text content
18
+ msg_type: int = 1 # 1=text, 3=image, 34=voice, 47=emoji, etc.
19
+ is_self: bool = False # sent by the account owner
20
+
21
+ @property
22
+ def formatted_time(self) -> str:
23
+ return self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
24
+
25
+ @property
26
+ def short_time(self) -> str:
27
+ return self.timestamp.strftime("%H:%M")
28
+
29
+ @property
30
+ def date_str(self) -> str:
31
+ return self.timestamp.strftime("%Y-%m-%d")
32
+
33
+
34
+ @dataclass
35
+ class Conversation:
36
+ """A conversation with a specific contact."""
37
+
38
+ contact_name: str
39
+ messages: list[Message] = field(default_factory=list)
40
+ my_name: str = "Me"
41
+
42
+ @property
43
+ def date_range(self) -> tuple[str, str]:
44
+ if not self.messages:
45
+ return ("", "")
46
+ first = self.messages[0].date_str
47
+ last = self.messages[-1].date_str
48
+ return (first, last)
49
+
50
+ @property
51
+ def message_count(self) -> int:
52
+ return len(self.messages)
53
+
54
+ def group_by_date(self) -> list[tuple[str, list[Message]]]:
55
+ """Group messages by date, returns [(date_str, [msg, ...]), ...]."""
56
+ groups: list[tuple[str, list[Message]]] = []
57
+ for msg in self.messages:
58
+ if not groups or groups[-1][0] != msg.date_str:
59
+ groups.append((msg.date_str, []))
60
+ groups[-1][1].append(msg)
61
+ return groups
@@ -0,0 +1,292 @@
1
+ """Parser: read WeChat messages from SQLite database or CSV/JSON exports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ import sqlite3
8
+ import re
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from .models import Conversation, Message
14
+
15
+
16
+ # ── SQLite (decrypted WeChat DB) ────────────────────────────────────────────
17
+
18
+ def _try_parse_ts(val) -> Optional[datetime]:
19
+ """WeChat uses Unix timestamps (seconds or milliseconds)."""
20
+ if val is None:
21
+ return None
22
+ try:
23
+ n = int(val)
24
+ if n > 1_000_000_000_000: # milliseconds
25
+ n //= 1000
26
+ return datetime.fromtimestamp(n)
27
+ except (ValueError, OSError):
28
+ return None
29
+
30
+
31
+ def parse_db(
32
+ db_path: str | Path,
33
+ contact_name: str,
34
+ my_name: str = "Me",
35
+ *,
36
+ contact_col: str = "StrTalker",
37
+ content_col: str = "StrContent",
38
+ time_col: str = "CreateTime",
39
+ type_col: str = "Type",
40
+ is_sender_col: str = "IsSender",
41
+ table: str = "MSG",
42
+ ) -> Conversation:
43
+ """
44
+ Parse a decrypted WeChat Msg database.
45
+
46
+ Parameters
47
+ ----------
48
+ db_path : str | Path
49
+ Path to the decrypted SQLite database (e.g. ``MSG0.db``).
50
+ contact_name : str
51
+ The wxid or display name of the contact whose conversation to export.
52
+ my_name : str
53
+ How to label your own messages (default "Me").
54
+ contact_col, content_col, time_col, type_col, is_sender_col, table :
55
+ Column / table names; defaults work for common WeChat versions.
56
+ """
57
+ db_path = Path(db_path)
58
+ if not db_path.exists():
59
+ raise FileNotFoundError(f"Database not found: {db_path}")
60
+
61
+ conn = sqlite3.connect(str(db_path))
62
+ conn.row_factory = sqlite3.Row
63
+ cur = conn.cursor()
64
+
65
+ # Determine the actual table name
66
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
67
+ tables = [r["name"] for r in cur.fetchall()]
68
+ if table not in tables:
69
+ # Try to find a suitable table
70
+ candidates = [t for t in tables if "MSG" in t.upper()]
71
+ if candidates:
72
+ table = candidates[0]
73
+ else:
74
+ raise ValueError(
75
+ f"Table '{table}' not found. Available tables: {tables}"
76
+ )
77
+
78
+ # Determine columns
79
+ cur.execute(f"PRAGMA table_info('{table}')")
80
+ cols = {r["name"].lower(): r["name"] for r in cur.fetchall()}
81
+
82
+ def _col(key: str) -> str:
83
+ return cols.get(key.lower(), key)
84
+
85
+ # Query messages for this contact (sorted by time)
86
+ try:
87
+ cur.execute(
88
+ f'SELECT * FROM "{table}" WHERE {_col(contact_col)} = ? ORDER BY {_col(time_col)} ASC',
89
+ (contact_name,),
90
+ )
91
+ except sqlite3.OperationalError as e:
92
+ # Maybe the contact is stored in another column
93
+ raise RuntimeError(
94
+ f"Failed to query table '{table}'. Tried contact column '{contact_col}'. "
95
+ f"Available columns: {list(cols.values())}. Error: {e}"
96
+ )
97
+
98
+ rows = cur.fetchall()
99
+ conn.close()
100
+
101
+ messages: list[Message] = []
102
+ for row in rows:
103
+ ts = _try_parse_ts(row[_col(time_col)])
104
+ content = row[_col(content_col)] or ""
105
+ msg_type = int(row[_col(type_col)] or 1)
106
+
107
+ # Determine if self-sent
108
+ is_self = False
109
+ raw_is_sender = row[_col(is_sender_col)]
110
+ if raw_is_sender is not None:
111
+ is_self = bool(int(raw_is_sender))
112
+
113
+ # Skip non-text system messages
114
+ if msg_type == 10000: # system message
115
+ continue
116
+
117
+ # Format non-text messages
118
+ if msg_type != 1:
119
+ content = _format_media(content, msg_type)
120
+
121
+ messages.append(Message(
122
+ id=row.get("localId", row.get("msgId", len(messages))),
123
+ timestamp=ts or datetime.now(),
124
+ sender_name=my_name if is_self else contact_name,
125
+ content=content,
126
+ msg_type=msg_type,
127
+ is_self=is_self,
128
+ ))
129
+
130
+ if not messages:
131
+ print(f"⚠ No messages found for contact '{contact_name}' in table '{table}'.")
132
+
133
+ return Conversation(contact_name=contact_name, messages=messages, my_name=my_name)
134
+
135
+
136
+ def _format_media(content: str, msg_type: int) -> str:
137
+ """Replace binary/media placeholders with readable labels."""
138
+ type_labels = {
139
+ 3: "[图片]",
140
+ 34: "[语音]",
141
+ 43: "[视频]",
142
+ 47: "[表情]",
143
+ 49: "[链接/文件]",
144
+ 10000: "[系统消息]",
145
+ 10002: "[群系统消息]",
146
+ }
147
+ label = type_labels.get(msg_type, f"[消息类型:{msg_type}]")
148
+
149
+ # Try to extract useful text from XML content
150
+ if msg_type == 49 and content:
151
+ title = re.search(r"<title>(.*?)</title>", content)
152
+ if title:
153
+ label = f"[链接] {title.group(1)}"
154
+ desc = re.search(r"<des>(.*?)</des>", content)
155
+ if desc:
156
+ label += f"\n{desc.group(1)}"
157
+
158
+ return f"{label}"
159
+
160
+
161
+ # ── CSV 导出 ─────────────────────────────────────────────────────────────────
162
+
163
+ def parse_csv(
164
+ csv_path: str | Path,
165
+ contact_name: str,
166
+ my_name: str = "Me",
167
+ *,
168
+ encoding: str = "utf-8-sig",
169
+ ) -> Conversation:
170
+ """
171
+ Parse a CSV export with columns: timestamp, sender, content[, type].
172
+
173
+ The CSV can be exported by tools like 微信聊天记录导出 or similar.
174
+ """
175
+ csv_path = Path(csv_path)
176
+ if not csv_path.exists():
177
+ raise FileNotFoundError(f"CSV file not found: {csv_path}")
178
+
179
+ messages: list[Message] = []
180
+ with open(csv_path, encoding=encoding, newline="") as f:
181
+ reader = csv.DictReader(f)
182
+ for i, row in enumerate(reader):
183
+ # try common column names
184
+ ts_str = row.get("timestamp") or row.get("time") or row.get("CreateTime") or ""
185
+ sender = row.get("sender") or row.get("talker") or row.get("StrTalker") or ""
186
+ content = row.get("content") or row.get("message") or row.get("StrContent") or ""
187
+ msg_type = int(row.get("type") or row.get("Type") or 1)
188
+
189
+ ts = _try_parse_ts(ts_str)
190
+ if ts is None:
191
+ try:
192
+ ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
193
+ except ValueError:
194
+ ts = datetime.now()
195
+
196
+ is_self = sender.strip().lower() in (my_name.lower(), "me", "1", "true")
197
+ if is_self:
198
+ display_sender = my_name
199
+ else:
200
+ display_sender = sender.strip()
201
+
202
+ content = content or ""
203
+ if msg_type != 1:
204
+ content = _format_media(content, msg_type)
205
+
206
+ messages.append(Message(
207
+ id=i,
208
+ timestamp=ts,
209
+ sender_name=display_sender,
210
+ content=content,
211
+ msg_type=msg_type,
212
+ is_self=is_self,
213
+ ))
214
+
215
+ return Conversation(contact_name=contact_name, messages=messages, my_name=my_name)
216
+
217
+
218
+ # ── JSON 导出 ─────────────────────────────────────────────────────────────────
219
+
220
+ def parse_json(
221
+ json_path: str | Path,
222
+ contact_name: str,
223
+ my_name: str = "Me",
224
+ *,
225
+ encoding: str = "utf-8",
226
+ ) -> Conversation:
227
+ """
228
+ Parse a JSON export (list of message objects).
229
+
230
+ Each object should have: timestamp (or time), sender (or talker), content (or message).
231
+ """
232
+ json_path = Path(json_path)
233
+ if not json_path.exists():
234
+ raise FileNotFoundError(f"JSON file not found: {json_path}")
235
+
236
+ data = json.loads(json_path.read_text(encoding=encoding))
237
+ if isinstance(data, dict):
238
+ data = data.get("messages") or data.get("data") or []
239
+
240
+ messages: list[Message] = []
241
+ for i, obj in enumerate(data):
242
+ ts_str = obj.get("timestamp") or obj.get("time") or obj.get("CreateTime") or ""
243
+ sender = obj.get("sender") or obj.get("talker") or obj.get("StrTalker") or ""
244
+ content = obj.get("content") or obj.get("message") or obj.get("StrContent") or ""
245
+ msg_type = int(obj.get("type") or obj.get("Type") or 1)
246
+
247
+ ts = _try_parse_ts(ts_str)
248
+ if ts is None:
249
+ try:
250
+ ts = datetime.strptime(str(ts_str), "%Y-%m-%d %H:%M:%S")
251
+ except ValueError:
252
+ ts = datetime.now()
253
+
254
+ is_self = str(sender).strip().lower() in (my_name.lower(), "me", "1", "true")
255
+ display_sender = my_name if is_self else str(sender).strip()
256
+
257
+ if msg_type != 1:
258
+ content = _format_media(content, msg_type)
259
+
260
+ messages.append(Message(
261
+ id=i,
262
+ timestamp=ts,
263
+ sender_name=display_sender,
264
+ content=content,
265
+ msg_type=msg_type,
266
+ is_self=is_self,
267
+ ))
268
+
269
+ return Conversation(contact_name=contact_name, messages=messages, my_name=my_name)
270
+
271
+
272
+ # ── Auto-detect ──────────────────────────────────────────────────────────────
273
+
274
+ def auto_parse(
275
+ path: str | Path,
276
+ contact_name: str,
277
+ my_name: str = "Me",
278
+ **kwargs,
279
+ ) -> Conversation:
280
+ """Auto-detect file type and parse accordingly."""
281
+ path = Path(path)
282
+ suffix = path.suffix.lower()
283
+
284
+ if suffix in (".db", ".sqlite", ".sqlite3", ""):
285
+ return parse_db(path, contact_name, my_name, **kwargs)
286
+ elif suffix == ".csv":
287
+ return parse_csv(path, contact_name, my_name, **kwargs)
288
+ elif suffix == ".json":
289
+ return parse_json(path, contact_name, my_name, **kwargs)
290
+ else:
291
+ # try as db first
292
+ return parse_db(path, contact_name, my_name, **kwargs)
@@ -0,0 +1,193 @@
1
+ """HTML template for WeChat conversation export."""
2
+
3
+ HTML_TEMPLATE = r"""<!DOCTYPE html>
4
+ <html lang="zh-CN">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>与 {{ conv.contact_name }} 的微信聊天记录</title>
9
+ <style>
10
+ :root {
11
+ --bg: #ededed;
12
+ --card-bg: #ffffff;
13
+ --bubble-self: #95ec69;
14
+ --bubble-other: #ffffff;
15
+ --text: #111;
16
+ --muted: #999;
17
+ --border: #d9d9d9;
18
+ --date-bg: #dcdcdc;
19
+ }
20
+ * { margin:0; padding:0; box-sizing:border-box; }
21
+ body {
22
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
23
+ "Microsoft YaHei", "Helvetica Neue", sans-serif;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ line-height: 1.6;
27
+ }
28
+
29
+ /* ── header ── */
30
+ .header {
31
+ background: var(--card-bg);
32
+ padding: 32px 24px 24px;
33
+ text-align: center;
34
+ border-bottom: 1px solid var(--border);
35
+ position: sticky;
36
+ top: 0;
37
+ z-index: 10;
38
+ box-shadow: 0 1px 6px rgba(0,0,0,.06);
39
+ }
40
+ .header h1 { font-size: 1.4rem; font-weight: 600; margin-bottom: 4px; }
41
+ .header .meta { font-size: 0.85rem; color: var(--muted); }
42
+
43
+ /* ── container ── */
44
+ .container {
45
+ max-width: 680px;
46
+ margin: 0 auto;
47
+ padding: 16px 12px 48px;
48
+ }
49
+
50
+ /* ── date divider ── */
51
+ .date-divider {
52
+ text-align: center;
53
+ margin: 20px 0 12px;
54
+ }
55
+ .date-divider span {
56
+ display: inline-block;
57
+ background: var(--date-bg);
58
+ color: #666;
59
+ font-size: 0.75rem;
60
+ padding: 3px 12px;
61
+ border-radius: 3px;
62
+ }
63
+
64
+ /* ── message row ── */
65
+ .msg-row {
66
+ display: flex;
67
+ margin-bottom: 12px;
68
+ padding: 0 8px;
69
+ }
70
+ .msg-row.self { justify-content: flex-end; }
71
+ .msg-row.other { justify-content: flex-start; }
72
+
73
+ /* ── bubble ── */
74
+ .bubble {
75
+ max-width: 72%;
76
+ padding: 10px 14px;
77
+ border-radius: 8px;
78
+ font-size: 0.95rem;
79
+ word-break: break-word;
80
+ white-space: pre-wrap;
81
+ position: relative;
82
+ box-shadow: 0 1px 2px rgba(0,0,0,.06);
83
+ }
84
+ .self .bubble {
85
+ background: var(--bubble-self);
86
+ border-top-right-radius: 2px;
87
+ }
88
+ .other .bubble {
89
+ background: var(--bubble-other);
90
+ border: 1px solid var(--border);
91
+ border-top-left-radius: 2px;
92
+ }
93
+
94
+ /* ── sender label ── */
95
+ .sender-label {
96
+ font-size: 0.7rem;
97
+ color: var(--muted);
98
+ margin-bottom: 2px;
99
+ padding: 0 4px;
100
+ }
101
+ .self .sender-label { text-align: right; }
102
+
103
+ /* ── time ── */
104
+ .time-tag {
105
+ font-size: 0.65rem;
106
+ color: #b0b0b0;
107
+ margin-top: 4px;
108
+ text-align: right;
109
+ }
110
+
111
+ /* ── media tag ── */
112
+ .media-tag {
113
+ color: #5865a0;
114
+ font-style: italic;
115
+ }
116
+
117
+ /* ── footer ── */
118
+ .footer {
119
+ text-align: center;
120
+ padding: 24px;
121
+ color: var(--muted);
122
+ font-size: 0.78rem;
123
+ }
124
+ .footer a { color: var(--muted); }
125
+
126
+ /* ── stats bar ── */
127
+ .stats {
128
+ display: flex;
129
+ justify-content: center;
130
+ gap: 18px;
131
+ flex-wrap: wrap;
132
+ margin-top: 8px;
133
+ font-size: 0.8rem;
134
+ color: var(--muted);
135
+ }
136
+
137
+ @media (max-width: 480px) {
138
+ .container { padding: 8px 4px 40px; }
139
+ .bubble { max-width: 85%; font-size: 0.9rem; }
140
+ .header { padding: 20px 12px 16px; }
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+
146
+ <div class="header">
147
+ <h1>💬 {{ conv.contact_name }}</h1>
148
+ <div class="meta">微信聊天记录</div>
149
+ <div class="stats">
150
+ {% if conv.messages %}
151
+ <span>📅 {{ conv.messages[0].date_str }} — {{ conv.messages[-1].date_str }}</span>
152
+ <span>📝 {{ conv.message_count }} 条消息</span>
153
+ {% endif %}
154
+ </div>
155
+ </div>
156
+
157
+ <div class="container">
158
+ {% for date, msgs in conv.group_by_date() %}
159
+ <div class="date-divider"><span>{{ date }}</span></div>
160
+ {% for m in msgs %}
161
+ <div class="msg-row {{ 'self' if m.is_self else 'other' }}">
162
+ <div>
163
+ {% if not m.is_self %}
164
+ <div class="sender-label">{{ m.sender_name }}</div>
165
+ {% endif %}
166
+ <div class="bubble">
167
+ <span class="{{ 'media-tag' if m.msg_type != 1 else '' }}">{{ m.content }}</span>
168
+ <div class="time-tag">{{ m.short_time }}</div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ {% endfor %}
173
+ {% endfor %}
174
+ </div>
175
+
176
+ <div class="footer">
177
+ <p>由 <a href="https://pypi.org/project/daochu/">DaoChu</a> 导出 · {{ now }}</p>
178
+ </div>
179
+
180
+ </body>
181
+ </html>"""
182
+
183
+
184
+ def render_html(conv, now_str: str = "") -> str:
185
+ """Render a Conversation to a complete HTML page."""
186
+ from jinja2 import Template
187
+ from datetime import datetime
188
+
189
+ tpl = Template(HTML_TEMPLATE)
190
+ return tpl.render(
191
+ conv=conv,
192
+ now=now_str or datetime.now().strftime("%Y-%m-%d %H:%M"),
193
+ )
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: daochu
3
+ Version: 0.1.0
4
+ Summary: 导出微信聊天记录为 HTML / Markdown — Export WeChat conversations to HTML or Markdown
5
+ Author-email: DaoChu Contributors <daochu@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/daochu/daochu
8
+ Project-URL: Repository, https://github.com/daochu/daochu
9
+ Keywords: wechat,export,chat,html,markdown,backup
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Communications :: Chat
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: click>=8.0
23
+ Requires-Dist: jinja2>=3.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.0; extra == "dev"
26
+ Requires-Dist: twine>=5.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # DaoChu(导出)
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/daochu.svg)](https://pypi.org/project/daochu/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/daochu.svg)](https://pypi.org/project/daochu/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ **DaoChu**(导出)是一个 Python 命令行工具,能将微信聊天记录导出为精美的 **HTML** 页面或 **Markdown** 文件。
36
+
37
+ > 支持解密后的微信 SQLite 数据库、CSV 导出、JSON 导出三种数据源。
38
+
39
+ ---
40
+
41
+ ## ✨ 特性
42
+
43
+ - 🎨 **精美 HTML 输出** — 仿微信气泡样式,带日期分组、响应式设计
44
+ - 📝 **Markdown 输出** — 纯文本,适合归档或导入笔记工具
45
+ - 🔍 **自动联系人发现** — `list-contacts` 命令快速查看所有联系人
46
+ - 🧠 **智能解析** — 自动识别文件类型(.db / .csv / .json)
47
+ - 🖼️ **媒体标注** — 图片、语音、视频、链接等自动标注为可读文本
48
+
49
+ ---
50
+
51
+ ## 📦 安装
52
+
53
+ ```bash
54
+ pip install daochu
55
+ ```
56
+
57
+ 要求 Python ≥ 3.10。
58
+
59
+ ---
60
+
61
+ ## 🚀 快速开始
62
+
63
+ ### 1. 列出联系人
64
+
65
+ ```bash
66
+ daochu list-contacts /path/to/decrypted/MSG.db
67
+ ```
68
+
69
+ ### 2. 导出聊天记录
70
+
71
+ ```bash
72
+ # 导出为 HTML(默认)
73
+ daochu export MSG.db "wxid_abc123"
74
+
75
+ # 导出为 Markdown
76
+ daochu export MSG.db "wxid_abc123" -f md
77
+
78
+ # 指定输出路径
79
+ daochu export MSG.db "wxid_abc123" -o ~/Desktop/chat.html
80
+
81
+ # 自定义自己的显示名
82
+ daochu export MSG.db "wxid_abc123" --me "小明"
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 📂 数据源说明
88
+
89
+ | 类型 | 说明 |
90
+ |------|------|
91
+ | **SQLite (.db)** | 解密后的微信 Msg 数据库,最常见的方式 |
92
+ | **CSV (.csv)** | 从第三方工具导出的 CSV,需含 timestamp/sender/content 列 |
93
+ | **JSON (.json)** | 消息对象数组,格式 `[{timestamp, sender, content}]` |
94
+
95
+ > **如何获取解密数据库?** 微信数据库默认加密存储在 `Documents/WeChat Files/<wxid>/Msg/` 下。可使用开源工具解密后配合 DaoChu 使用。
96
+
97
+ ---
98
+
99
+ ## 🖥️ 命令行参考
100
+
101
+ ```
102
+ Usage: daochu [OPTIONS] COMMAND [ARGS]...
103
+
104
+ Commands:
105
+ export 导出与某个联系人的全部聊天记录
106
+ list-contacts 列出数据库中的所有联系人
107
+
108
+ export 选项:
109
+ -o, --output PATH 输出目录或文件路径
110
+ -f, --format [html|md] 输出格式
111
+ --me TEXT 自己的显示名称
112
+ --source-type [auto|db|csv|json]
113
+ --table TEXT 数据库表名
114
+ --encoding TEXT 文件编码
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🐍 Python API
120
+
121
+ ```python
122
+ from daochu.parser import parse_db
123
+ from daochu.exporter import export
124
+
125
+ conv = parse_db("MSG0.db", "wxid_abc123", my_name="我")
126
+ export(conv, "output.html", fmt="html")
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 🖼️ 效果预览
132
+
133
+ HTML 输出效果:
134
+
135
+ - 顶部:联系人名称、日期范围、消息总数
136
+ - 每一条消息:微信风格气泡,自己的消息靠右显示为绿色
137
+ - 日期分组:按天自动插入日期分隔线
138
+ - 移动端适配:响应式设计,手机上也能流畅阅读
139
+
140
+ ---
141
+
142
+ ## 📄 License
143
+
144
+ MIT © DaoChu Contributors
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ daochu/__init__.py
5
+ daochu/cli.py
6
+ daochu/exporter.py
7
+ daochu/models.py
8
+ daochu/parser.py
9
+ daochu/templates.py
10
+ daochu.egg-info/PKG-INFO
11
+ daochu.egg-info/SOURCES.txt
12
+ daochu.egg-info/dependency_links.txt
13
+ daochu.egg-info/entry_points.txt
14
+ daochu.egg-info/requires.txt
15
+ daochu.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ daochu = daochu.cli:main
@@ -0,0 +1,6 @@
1
+ click>=8.0
2
+ jinja2>=3.0
3
+
4
+ [dev]
5
+ build>=1.0
6
+ twine>=5.0
@@ -0,0 +1,2 @@
1
+ daochu
2
+ dist
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "daochu"
7
+ version = "0.1.0"
8
+ description = "导出微信聊天记录为 HTML / Markdown — Export WeChat conversations to HTML or Markdown"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ {name = "DaoChu Contributors", email = "daochu@example.com"}
14
+ ]
15
+ keywords = ["wechat", "export", "chat", "html", "markdown", "backup"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Communications :: Chat",
25
+ "Topic :: Utilities",
26
+ ]
27
+ requires-python = ">=3.10"
28
+ dependencies = [
29
+ "click>=8.0",
30
+ "jinja2>=3.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "build>=1.0",
36
+ "twine>=5.0",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/daochu/daochu"
41
+ Repository = "https://github.com/daochu/daochu"
42
+
43
+ [project.scripts]
44
+ daochu = "daochu.cli:main"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["."]
daochu-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+