git-ew 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.
- git_ew/__init__.py +166 -0
- git_ew/__main__.py +32 -0
- git_ew/_internal/__init__.py +17 -0
- git_ew/_internal/app.py +418 -0
- git_ew/_internal/cli.py +246 -0
- git_ew/_internal/config.py +148 -0
- git_ew/_internal/database.py +297 -0
- git_ew/_internal/debug.py +130 -0
- git_ew/_internal/email_fetcher.py +301 -0
- git_ew/_internal/email_parser.py +274 -0
- git_ew/_internal/email_sender.py +220 -0
- git_ew/_internal/mailing_lists/__init__.py +19 -0
- git_ew/_internal/mailing_lists/zsh_workers/__init__.py +17 -0
- git_ew/_internal/mailing_lists/zsh_workers/ingest.py +427 -0
- git_ew/_internal/mailing_lists/zsh_workers/sync_archives.py +300 -0
- git_ew/_internal/models.py +149 -0
- git_ew/_internal/secrets.py +59 -0
- git_ew/_internal/sync.py +156 -0
- git_ew/_internal/thread_utils.py +185 -0
- git_ew/py.typed +0 -0
- git_ew/static/css/style.css +608 -0
- git_ew/static/js/main.js +47 -0
- git_ew/templates/base.html +38 -0
- git_ew/templates/index.html +44 -0
- git_ew/templates/thread.html +153 -0
- git_ew-0.1.0.dist-info/METADATA +291 -0
- git_ew-0.1.0.dist-info/RECORD +30 -0
- git_ew-0.1.0.dist-info/WHEEL +4 -0
- git_ew-0.1.0.dist-info/entry_points.txt +5 -0
- git_ew-0.1.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SPDX-License-Identifier: ISC
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2021, Timothée Mazzucotelli and contributors
|
|
4
|
+
|
|
5
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
# purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
# copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
# Thread organization and rendering utilities for git-ew.
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
import markdown
|
|
25
|
+
from pymdownx.emoji import to_svg, twemoji
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from git_ew._internal.models import Message
|
|
29
|
+
|
|
30
|
+
# Markdown instance with configured extensions
|
|
31
|
+
_md = markdown.Markdown(
|
|
32
|
+
extensions=[
|
|
33
|
+
"abbr",
|
|
34
|
+
"admonition",
|
|
35
|
+
"attr_list",
|
|
36
|
+
"def_list",
|
|
37
|
+
"footnotes",
|
|
38
|
+
"md_in_html",
|
|
39
|
+
"nl2br",
|
|
40
|
+
"toc",
|
|
41
|
+
"pymdownx.arithmatex",
|
|
42
|
+
"pymdownx.betterem",
|
|
43
|
+
"pymdownx.caret",
|
|
44
|
+
"pymdownx.details",
|
|
45
|
+
"pymdownx.emoji",
|
|
46
|
+
"pymdownx.highlight",
|
|
47
|
+
"pymdownx.inlinehilite",
|
|
48
|
+
"pymdownx.keys",
|
|
49
|
+
"pymdownx.magiclink",
|
|
50
|
+
"pymdownx.mark",
|
|
51
|
+
"pymdownx.smartsymbols",
|
|
52
|
+
"pymdownx.superfences",
|
|
53
|
+
"pymdownx.tabbed",
|
|
54
|
+
"pymdownx.tasklist",
|
|
55
|
+
"pymdownx.tilde",
|
|
56
|
+
],
|
|
57
|
+
extension_configs={
|
|
58
|
+
"toc": {"permalink": True},
|
|
59
|
+
"pymdownx.arithmatex": {"generic": True},
|
|
60
|
+
"pymdownx.betterem": {"smart_enable": "all"},
|
|
61
|
+
"pymdownx.emoji": {
|
|
62
|
+
"emoji_index": twemoji,
|
|
63
|
+
"emoji_generator": to_svg,
|
|
64
|
+
},
|
|
65
|
+
"pymdownx.tabbed": {"alternate_style": True},
|
|
66
|
+
"pymdownx.tasklist": {"custom_checkbox": True},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_markdown(text: str) -> str:
|
|
72
|
+
"""Render plain text as markdown HTML.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
text: Plain text to render.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
HTML string.
|
|
79
|
+
"""
|
|
80
|
+
_md.reset()
|
|
81
|
+
return _md.convert(text)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _split_quoted_reply(body: str) -> tuple[str, str]:
|
|
85
|
+
"""Split trailing quoted lines from an email body.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
body: Email body text.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The new text and trailing quoted text.
|
|
92
|
+
"""
|
|
93
|
+
if not body:
|
|
94
|
+
return "", ""
|
|
95
|
+
|
|
96
|
+
lines = body.rstrip().splitlines()
|
|
97
|
+
quote_start_idx = len(lines)
|
|
98
|
+
while quote_start_idx > 0 and lines[quote_start_idx - 1].startswith(">"):
|
|
99
|
+
quote_start_idx -= 1
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
"\n".join(lines[:quote_start_idx]).strip(),
|
|
103
|
+
"\n".join(lines[quote_start_idx:]).strip(),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ThreadNode:
|
|
109
|
+
"""Represents a node in a thread tree."""
|
|
110
|
+
|
|
111
|
+
message: Message
|
|
112
|
+
"""The message at this node."""
|
|
113
|
+
children: list[ThreadNode]
|
|
114
|
+
"""Child nodes in the thread tree."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class _RenderedMessage:
|
|
119
|
+
"""Store a message and its presentation-only content."""
|
|
120
|
+
|
|
121
|
+
message: Message
|
|
122
|
+
body: str
|
|
123
|
+
quoted_body: str
|
|
124
|
+
children: list[_RenderedMessage]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_thread_tree(messages: list[Message]) -> list[ThreadNode]:
|
|
128
|
+
"""Build a tree structure from a flat list of messages.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
messages: List of messages in the thread.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of root ThreadNodes.
|
|
135
|
+
"""
|
|
136
|
+
# Create nodes for each message
|
|
137
|
+
nodes = {msg.message_id: ThreadNode(message=msg, children=[]) for msg in messages}
|
|
138
|
+
|
|
139
|
+
# Build the tree by linking children to parents
|
|
140
|
+
roots = []
|
|
141
|
+
|
|
142
|
+
for msg in messages:
|
|
143
|
+
node = nodes[msg.message_id]
|
|
144
|
+
|
|
145
|
+
if msg.in_reply_to and msg.in_reply_to in nodes:
|
|
146
|
+
# This message is a reply to another message
|
|
147
|
+
parent_node = nodes[msg.in_reply_to]
|
|
148
|
+
parent_node.children.append(node)
|
|
149
|
+
else:
|
|
150
|
+
# This is a root message
|
|
151
|
+
roots.append(node)
|
|
152
|
+
|
|
153
|
+
return roots
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def thread_to_nested_structure(roots: list[ThreadNode], *, flatten: bool = True) -> list[_RenderedMessage]:
|
|
157
|
+
"""Convert thread tree to nested structure, with single-children popped out to sibling level.
|
|
158
|
+
|
|
159
|
+
Also detects and marks quoted sections in message bodies.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
roots: List of root ThreadNodes.
|
|
163
|
+
flatten: Place a sole child beside its parent when true.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Nested list of messages.
|
|
167
|
+
"""
|
|
168
|
+
result: list[_RenderedMessage] = []
|
|
169
|
+
for root in roots:
|
|
170
|
+
body, quoted_body = _split_quoted_reply(root.message.body)
|
|
171
|
+
rendered = _RenderedMessage(
|
|
172
|
+
message=root.message,
|
|
173
|
+
body=body,
|
|
174
|
+
quoted_body=quoted_body,
|
|
175
|
+
children=[],
|
|
176
|
+
)
|
|
177
|
+
if flatten and len(root.children) == 1:
|
|
178
|
+
result.append(rendered)
|
|
179
|
+
result.extend(thread_to_nested_structure(root.children, flatten=flatten))
|
|
180
|
+
elif root.children:
|
|
181
|
+
rendered.children = thread_to_nested_structure(root.children, flatten=flatten)
|
|
182
|
+
result.append(rendered)
|
|
183
|
+
else:
|
|
184
|
+
result.append(rendered)
|
|
185
|
+
return result
|
git_ew/py.typed
ADDED
|
File without changes
|