chgksuite 0.26.0b11__py3-none-any.whl → 0.27.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.
- chgksuite/_html2md.py +90 -0
- chgksuite/cli.py +38 -8
- chgksuite/common.py +16 -12
- chgksuite/composer/__init__.py +9 -7
- chgksuite/composer/chgksuite_parser.py +20 -9
- chgksuite/composer/composer_common.py +30 -3
- chgksuite/composer/db.py +1 -2
- chgksuite/composer/docx.py +542 -292
- chgksuite/composer/latex.py +3 -4
- chgksuite/composer/lj.py +1 -2
- chgksuite/composer/{reddit.py → markdown.py} +35 -25
- chgksuite/composer/openquiz.py +2 -3
- chgksuite/composer/pptx.py +18 -6
- chgksuite/composer/telegram.py +22 -10
- chgksuite/handouter/gen.py +11 -7
- chgksuite/handouter/installer.py +0 -0
- chgksuite/handouter/runner.py +237 -10
- chgksuite/handouter/tex_internals.py +12 -13
- chgksuite/handouter/utils.py +22 -1
- chgksuite/lastdir +1 -0
- chgksuite/parser.py +218 -37
- chgksuite/parser_db.py +4 -6
- chgksuite/resources/labels_az.toml +22 -0
- chgksuite/resources/labels_by.toml +1 -2
- chgksuite/resources/labels_by_tar.toml +1 -2
- chgksuite/resources/labels_en.toml +1 -2
- chgksuite/resources/labels_kz_cyr.toml +1 -2
- chgksuite/resources/labels_ru.toml +1 -2
- chgksuite/resources/labels_sr.toml +1 -2
- chgksuite/resources/labels_ua.toml +1 -2
- chgksuite/resources/labels_uz.toml +0 -3
- chgksuite/resources/labels_uz_cyr.toml +1 -2
- chgksuite/resources/regexes_az.json +17 -0
- chgksuite/resources/regexes_by.json +3 -2
- chgksuite/resources/regexes_by_tar.json +17 -0
- chgksuite/resources/regexes_en.json +3 -2
- chgksuite/resources/regexes_kz_cyr.json +3 -2
- chgksuite/resources/regexes_ru.json +3 -2
- chgksuite/resources/regexes_sr.json +3 -2
- chgksuite/resources/regexes_ua.json +3 -2
- chgksuite/resources/regexes_uz.json +16 -0
- chgksuite/resources/regexes_uz_cyr.json +3 -2
- chgksuite/trello.py +8 -9
- chgksuite/typotools.py +9 -8
- chgksuite/version.py +1 -1
- {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/METADATA +10 -19
- chgksuite-0.27.0.dist-info/RECORD +63 -0
- {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/WHEEL +1 -2
- chgksuite/composer/telegram_parser.py +0 -230
- chgksuite-0.26.0b11.dist-info/RECORD +0 -59
- chgksuite-0.26.0b11.dist-info/top_level.txt +0 -1
- {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/entry_points.txt +0 -0
- {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/licenses/LICENSE +0 -0
chgksuite/composer/latex.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import hashlib
|
|
3
2
|
import os
|
|
4
3
|
import re
|
|
@@ -199,11 +198,11 @@ class LatexExporter(BaseExporter):
|
|
|
199
198
|
firsttour = True
|
|
200
199
|
for element in self.structure:
|
|
201
200
|
if element[0] == "heading":
|
|
202
|
-
tex += "\n{{\\huge {}}}\n
|
|
201
|
+
tex += "\n{{\\huge {}}}\n\\vspace{{0.8em}}\n".format(
|
|
203
202
|
self.tex_element_layout(element[1])
|
|
204
203
|
)
|
|
205
204
|
if element[0] == "date":
|
|
206
|
-
tex += "\n{{\\large {}}}\n
|
|
205
|
+
tex += "\n{{\\large {}}}\n\\vspace{{0.8em}}\n".format(
|
|
207
206
|
self.tex_element_layout(element[1])
|
|
208
207
|
)
|
|
209
208
|
if element[0] in {"meta", "editor"}:
|
|
@@ -221,7 +220,7 @@ class LatexExporter(BaseExporter):
|
|
|
221
220
|
|
|
222
221
|
tex += "\\end{document}"
|
|
223
222
|
|
|
224
|
-
with
|
|
223
|
+
with open(outfilename, "w", encoding="utf-8") as outfile:
|
|
225
224
|
outfile.write(tex)
|
|
226
225
|
cwd = os.getcwd()
|
|
227
226
|
os.chdir(self.dir_kwargs["tmp_dir"])
|
chgksuite/composer/lj.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import datetime
|
|
3
2
|
import os
|
|
4
3
|
import random
|
|
@@ -240,7 +239,7 @@ class LjExporter(BaseExporter):
|
|
|
240
239
|
"general_impressions_text"
|
|
241
240
|
]
|
|
242
241
|
if self.args.debug:
|
|
243
|
-
with
|
|
242
|
+
with open("lj.debug", "w", encoding="utf-8") as f:
|
|
244
243
|
f.write(log_wrap(final_structure))
|
|
245
244
|
return final_structure
|
|
246
245
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import os
|
|
3
2
|
|
|
4
3
|
from chgksuite.composer.composer_common import (
|
|
@@ -9,20 +8,20 @@ from chgksuite.composer.composer_common import (
|
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
class
|
|
11
|
+
class MarkdownExporter(BaseExporter):
|
|
13
12
|
def __init__(self, *args, **kwargs):
|
|
14
13
|
super().__init__(*args, **kwargs)
|
|
15
14
|
self.im = Imgur(self.args.imgur_client_id or IMGUR_CLIENT_ID)
|
|
16
15
|
self.qcount = 1
|
|
17
16
|
|
|
18
|
-
def
|
|
17
|
+
def markdownyapper(self, e):
|
|
19
18
|
if isinstance(e, str):
|
|
20
|
-
return self.
|
|
19
|
+
return self.markdown_element_layout(e)
|
|
21
20
|
elif isinstance(e, list):
|
|
22
21
|
if not any(isinstance(x, list) for x in e):
|
|
23
|
-
return self.
|
|
22
|
+
return self.markdown_element_layout(e)
|
|
24
23
|
else:
|
|
25
|
-
return " \n".join([self.
|
|
24
|
+
return " \n".join([self.markdown_element_layout(x) for x in e])
|
|
26
25
|
|
|
27
26
|
def parse_and_upload_image(self, path):
|
|
28
27
|
parsed_image = parseimg(
|
|
@@ -37,11 +36,13 @@ class RedditExporter(BaseExporter):
|
|
|
37
36
|
imglink = uploaded_image["data"]["link"]
|
|
38
37
|
return imglink
|
|
39
38
|
|
|
40
|
-
def
|
|
39
|
+
def markdownformat(self, s):
|
|
41
40
|
res = ""
|
|
42
41
|
for run in self.parse_4s_elem(s):
|
|
43
|
-
if run[0]
|
|
42
|
+
if run[0] == "":
|
|
44
43
|
res += run[1]
|
|
44
|
+
if run[0] == "hyperlink":
|
|
45
|
+
res += "<{}>".format(run[1])
|
|
45
46
|
if run[0] == "screen":
|
|
46
47
|
res += run[1]["for_screen"]
|
|
47
48
|
if run[0] == "italic":
|
|
@@ -51,61 +52,70 @@ class RedditExporter(BaseExporter):
|
|
|
51
52
|
imglink = run[1]
|
|
52
53
|
else:
|
|
53
54
|
imglink = self.parse_and_upload_image(run[1])
|
|
54
|
-
|
|
55
|
+
if self.args.filetype == "redditmd":
|
|
56
|
+
res += "[картинка]({})".format(imglink)
|
|
57
|
+
else:
|
|
58
|
+
res += "".format(imglink)
|
|
55
59
|
while res.endswith("\n"):
|
|
56
60
|
res = res[:-1]
|
|
57
61
|
res = res.replace("\n", " \n")
|
|
58
62
|
return res
|
|
59
63
|
|
|
60
|
-
def
|
|
64
|
+
def markdown_element_layout(self, e):
|
|
61
65
|
res = ""
|
|
62
66
|
if isinstance(e, str):
|
|
63
|
-
res = self.
|
|
67
|
+
res = self.markdownformat(e)
|
|
64
68
|
return res
|
|
65
69
|
if isinstance(e, list):
|
|
66
70
|
res = " \n".join(
|
|
67
71
|
[
|
|
68
|
-
"{}\\. {}".format(i + 1, self.
|
|
72
|
+
"{}\\. {}".format(i + 1, self.markdown_element_layout(x))
|
|
69
73
|
for i, x in enumerate(e)
|
|
70
74
|
]
|
|
71
75
|
)
|
|
72
76
|
return res
|
|
73
77
|
|
|
74
|
-
def
|
|
78
|
+
def markdown_format_element(self, pair):
|
|
75
79
|
if pair[0] == "Question":
|
|
76
|
-
return self.
|
|
80
|
+
return self.markdown_format_question(pair[1])
|
|
77
81
|
|
|
78
|
-
def
|
|
82
|
+
def markdown_format_question(self, q):
|
|
79
83
|
if "setcounter" in q:
|
|
80
84
|
self.qcount = int(q["setcounter"])
|
|
81
85
|
res = "__Вопрос {}__: {} \n".format(
|
|
82
86
|
self.qcount if "number" not in q else q["number"],
|
|
83
|
-
self.
|
|
87
|
+
self.markdownyapper(q["question"]),
|
|
84
88
|
)
|
|
85
89
|
if "number" not in q:
|
|
86
90
|
self.qcount += 1
|
|
87
|
-
|
|
91
|
+
spoiler_start = ">!" if self.args.filetype == "redditmd" else ""
|
|
92
|
+
spoiler_end = "!<" if self.args.filetype == "redditmd" else ""
|
|
93
|
+
res += "__Ответ:__ {}{} \n".format(
|
|
94
|
+
spoiler_start, self.markdownyapper(q["answer"])
|
|
95
|
+
)
|
|
88
96
|
if "zachet" in q:
|
|
89
|
-
res += "__Зачёт:__ {} \n".format(self.
|
|
97
|
+
res += "__Зачёт:__ {} \n".format(self.markdownyapper(q["zachet"]))
|
|
90
98
|
if "nezachet" in q:
|
|
91
|
-
res += "__Незачёт:__ {} \n".format(self.
|
|
99
|
+
res += "__Незачёт:__ {} \n".format(self.markdownyapper(q["nezachet"]))
|
|
92
100
|
if "comment" in q:
|
|
93
|
-
res += "__Комментарий:__ {} \n".format(self.
|
|
101
|
+
res += "__Комментарий:__ {} \n".format(self.markdownyapper(q["comment"]))
|
|
94
102
|
if "source" in q:
|
|
95
|
-
res += "__Источник:__ {} \n".format(self.
|
|
103
|
+
res += "__Источник:__ {} \n".format(self.markdownyapper(q["source"]))
|
|
96
104
|
if "author" in q:
|
|
97
|
-
res += "
|
|
105
|
+
res += "{}\n__Автор:__ {} \n".format(
|
|
106
|
+
spoiler_end, self.markdownyapper(q["author"])
|
|
107
|
+
)
|
|
98
108
|
else:
|
|
99
|
-
res += "
|
|
109
|
+
res += spoiler_end + "\n"
|
|
100
110
|
return res
|
|
101
111
|
|
|
102
112
|
def export(self, outfile):
|
|
103
113
|
result = []
|
|
104
114
|
for pair in self.structure:
|
|
105
|
-
res = self.
|
|
115
|
+
res = self.markdown_format_element(pair)
|
|
106
116
|
if res:
|
|
107
117
|
result.append(res)
|
|
108
118
|
text = "\n\n".join(result)
|
|
109
|
-
with
|
|
119
|
+
with open(outfile, "w", encoding="utf-8") as f:
|
|
110
120
|
f.write(text)
|
|
111
121
|
self.logger.info("Output: {}".format(outfile))
|
chgksuite/composer/openquiz.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import copy
|
|
3
2
|
import re
|
|
4
3
|
import json
|
|
@@ -71,7 +70,7 @@ class OpenquizExporter(BaseExporter):
|
|
|
71
70
|
)
|
|
72
71
|
while res.endswith("\n"):
|
|
73
72
|
res = res[:-1]
|
|
74
|
-
hs = self.
|
|
73
|
+
hs = self.regexes["handout_short"]
|
|
75
74
|
if images:
|
|
76
75
|
res = re.sub("\\[" + hs + "(.+?)\\]", "", s, flags=re.DOTALL)
|
|
77
76
|
res = res.strip()
|
|
@@ -175,5 +174,5 @@ class OpenquizExporter(BaseExporter):
|
|
|
175
174
|
result = []
|
|
176
175
|
for q in questions:
|
|
177
176
|
result.append(self.oq_format_question(q))
|
|
178
|
-
with
|
|
177
|
+
with open(outfilename, "w", encoding="utf-8") as f:
|
|
179
178
|
f.write(json.dumps(result, indent=2, ensure_ascii=False))
|
chgksuite/composer/pptx.py
CHANGED
|
@@ -5,7 +5,12 @@ import re
|
|
|
5
5
|
import toml
|
|
6
6
|
|
|
7
7
|
from chgksuite.common import log_wrap, replace_escaped, tryint
|
|
8
|
-
from chgksuite.composer.composer_common import
|
|
8
|
+
from chgksuite.composer.composer_common import (
|
|
9
|
+
BaseExporter,
|
|
10
|
+
backtick_replace,
|
|
11
|
+
parseimg,
|
|
12
|
+
remove_accents_standalone,
|
|
13
|
+
)
|
|
9
14
|
from pptx import Presentation
|
|
10
15
|
from pptx.dml.color import RGBColor
|
|
11
16
|
from pptx.enum.text import MSO_AUTO_SIZE, MSO_VERTICAL_ANCHOR, PP_ALIGN
|
|
@@ -109,15 +114,20 @@ class PptxExporter(BaseExporter):
|
|
|
109
114
|
r.font.underline = True
|
|
110
115
|
|
|
111
116
|
def pptx_process_text(
|
|
112
|
-
self,
|
|
117
|
+
self,
|
|
118
|
+
s,
|
|
119
|
+
image=None,
|
|
120
|
+
strip_brackets=True,
|
|
121
|
+
replace_spaces=True,
|
|
122
|
+
do_not_remove_accents=False,
|
|
113
123
|
):
|
|
114
|
-
hs = self.
|
|
124
|
+
hs = self.regexes["handout_short"]
|
|
115
125
|
if isinstance(s, list):
|
|
116
126
|
for i in range(len(s)):
|
|
117
127
|
s[i] = self.pptx_process_text(s[i], image=image)
|
|
118
128
|
return s
|
|
119
|
-
if not self.args.do_not_remove_accents:
|
|
120
|
-
s = s
|
|
129
|
+
if not (self.args.do_not_remove_accents or do_not_remove_accents):
|
|
130
|
+
s = remove_accents_standalone(s, self.regexes)
|
|
121
131
|
if strip_brackets:
|
|
122
132
|
s = self.remove_square_brackets(s)
|
|
123
133
|
s = s.replace("]\n", "]\n\n")
|
|
@@ -408,7 +418,9 @@ class PptxExporter(BaseExporter):
|
|
|
408
418
|
if number is not None:
|
|
409
419
|
self.set_question_number(slide, number)
|
|
410
420
|
p = self.init_paragraph(tf, text=handout)
|
|
411
|
-
self.pptx_format(
|
|
421
|
+
self.pptx_format(
|
|
422
|
+
self.pptx_process_text(handout, do_not_remove_accents=True), p, tf, slide
|
|
423
|
+
)
|
|
412
424
|
|
|
413
425
|
def process_question_text(self, q):
|
|
414
426
|
image = self._get_image_from_4s(q["question"])
|
chgksuite/composer/telegram.py
CHANGED
|
@@ -6,6 +6,7 @@ import sqlite3
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
8
|
import uuid
|
|
9
|
+
from typing import Optional, Union
|
|
9
10
|
|
|
10
11
|
import requests
|
|
11
12
|
import toml
|
|
@@ -126,7 +127,9 @@ class TelegramExporter(BaseExporter):
|
|
|
126
127
|
if result:
|
|
127
128
|
msg_data = json.loads(result["raw_data"])
|
|
128
129
|
if msg_data["message"]["chat"]["type"] != "private":
|
|
129
|
-
print(
|
|
130
|
+
print(
|
|
131
|
+
"You should post to the PRIVATE chat, not to the channel/group"
|
|
132
|
+
)
|
|
130
133
|
continue
|
|
131
134
|
self.control_chat_id = msg_data["message"]["chat"]["id"]
|
|
132
135
|
self.send_api_request(
|
|
@@ -245,7 +248,7 @@ class TelegramExporter(BaseExporter):
|
|
|
245
248
|
channel_id_str = channel_id_str[4:]
|
|
246
249
|
return f"https://t.me/c/{channel_id_str}/{message_id}"
|
|
247
250
|
|
|
248
|
-
def extract_id_from_link(self, link) -> int
|
|
251
|
+
def extract_id_from_link(self, link) -> Optional[Union[int, str]]:
|
|
249
252
|
"""
|
|
250
253
|
Extract channel or chat ID from a Telegram link.
|
|
251
254
|
Examples:
|
|
@@ -869,7 +872,7 @@ class TelegramExporter(BaseExporter):
|
|
|
869
872
|
raise Exception("Failed to get channel ID from forwarded message")
|
|
870
873
|
else:
|
|
871
874
|
raise Exception("Channel ID is undefined")
|
|
872
|
-
|
|
875
|
+
|
|
873
876
|
# Handle chat resolution
|
|
874
877
|
if isinstance(chat_result, int):
|
|
875
878
|
chat_id = chat_result
|
|
@@ -881,7 +884,9 @@ class TelegramExporter(BaseExporter):
|
|
|
881
884
|
f"Please write a message in the discussion group with text: {self.chat_auth_uuid}"
|
|
882
885
|
)
|
|
883
886
|
print("This will allow me to extract the group ID automatically.")
|
|
884
|
-
print(
|
|
887
|
+
print(
|
|
888
|
+
"The bot MUST be added do the group and made admin, else it won't work!"
|
|
889
|
+
)
|
|
885
890
|
print("=" * 50 + "\n")
|
|
886
891
|
|
|
887
892
|
# Wait for a forwarded message with chat information
|
|
@@ -1158,7 +1163,10 @@ class TelegramExporter(BaseExporter):
|
|
|
1158
1163
|
if get_text(msg_data) != self.chat_auth_uuid:
|
|
1159
1164
|
continue
|
|
1160
1165
|
extracted_id = msg_data["message"]["chat"]["id"]
|
|
1161
|
-
if
|
|
1166
|
+
if (
|
|
1167
|
+
extracted_id == channel_numeric_id
|
|
1168
|
+
or extracted_id == self.control_chat_id
|
|
1169
|
+
):
|
|
1162
1170
|
self.logger.warning(
|
|
1163
1171
|
"User posted a message in the channel, not the discussion group"
|
|
1164
1172
|
)
|
|
@@ -1168,7 +1176,7 @@ class TelegramExporter(BaseExporter):
|
|
|
1168
1176
|
"chat_id": self.control_chat_id,
|
|
1169
1177
|
"text": (
|
|
1170
1178
|
"⚠️ You posted a message in the channel, not in the discussion group."
|
|
1171
|
-
)
|
|
1179
|
+
),
|
|
1172
1180
|
},
|
|
1173
1181
|
)
|
|
1174
1182
|
# Skip this message and continue waiting
|
|
@@ -1176,7 +1184,10 @@ class TelegramExporter(BaseExporter):
|
|
|
1176
1184
|
elif entity_type == "channel":
|
|
1177
1185
|
if msg_data["message"]["chat"]["id"] != self.control_chat_id:
|
|
1178
1186
|
continue
|
|
1179
|
-
if
|
|
1187
|
+
if (
|
|
1188
|
+
"message" in msg_data
|
|
1189
|
+
and "forward_from_chat" in msg_data["message"]
|
|
1190
|
+
):
|
|
1180
1191
|
forward_info = msg_data["message"]["forward_from_chat"]
|
|
1181
1192
|
|
|
1182
1193
|
# Extract chat ID from the message
|
|
@@ -1187,9 +1198,10 @@ class TelegramExporter(BaseExporter):
|
|
|
1187
1198
|
else:
|
|
1188
1199
|
extracted_id = chat_id
|
|
1189
1200
|
# For channels, check the type; for chats, accept any type except "channel" if check_type is False
|
|
1190
|
-
if extracted_id and (
|
|
1191
|
-
|
|
1192
|
-
|
|
1201
|
+
if extracted_id and (
|
|
1202
|
+
(check_type and forward_info.get("type") == "channel")
|
|
1203
|
+
or (not check_type)
|
|
1204
|
+
):
|
|
1193
1205
|
resolved = True
|
|
1194
1206
|
self.created_at = row["created_at"]
|
|
1195
1207
|
self.logger.info(
|
chgksuite/handouter/gen.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
import itertools
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
from collections import defaultdict
|
|
@@ -53,13 +54,16 @@ def generate_handouts_list(handouts, output_dir, base_name, parsed):
|
|
|
53
54
|
|
|
54
55
|
def generate_handouts(args):
|
|
55
56
|
_, resourcedir = get_source_dirs()
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
toml.loads(read_file(os.path.join(resourcedir, f"labels_{args.language}.toml")))
|
|
58
|
+
with open(
|
|
59
|
+
os.path.join(resourcedir, f"regexes_{args.language}.json"), encoding="utf8"
|
|
60
|
+
) as f:
|
|
61
|
+
regexes = json.load(f)
|
|
62
|
+
handout_re_text = (
|
|
63
|
+
"\\[" + regexes["handout_short"] + ".+?:( |\n)(?P<handout_text>.+?)\\]"
|
|
58
64
|
)
|
|
59
65
|
handout_re = re.compile(
|
|
60
|
-
|
|
61
|
-
+ labels["question_labels"]["handout_short"]
|
|
62
|
-
+ ".+?:( |\n)(?P<handout_text>.+?)\\]",
|
|
66
|
+
handout_re_text,
|
|
63
67
|
flags=re.DOTALL,
|
|
64
68
|
)
|
|
65
69
|
|
|
@@ -82,9 +86,9 @@ def generate_handouts(args):
|
|
|
82
86
|
if img:
|
|
83
87
|
try:
|
|
84
88
|
parsed_img = parseimg(img[0][1])
|
|
85
|
-
except:
|
|
89
|
+
except Exception as e:
|
|
86
90
|
print(
|
|
87
|
-
f"Image file for question {q['number']} not found, add it by hand"
|
|
91
|
+
f"Image file for question {q['number']} not found, add it by hand (exception {type(e)} {e})"
|
|
88
92
|
)
|
|
89
93
|
continue
|
|
90
94
|
else:
|
chgksuite/handouter/installer.py
CHANGED
|
File without changes
|