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.
Files changed (53) hide show
  1. chgksuite/_html2md.py +90 -0
  2. chgksuite/cli.py +38 -8
  3. chgksuite/common.py +16 -12
  4. chgksuite/composer/__init__.py +9 -7
  5. chgksuite/composer/chgksuite_parser.py +20 -9
  6. chgksuite/composer/composer_common.py +30 -3
  7. chgksuite/composer/db.py +1 -2
  8. chgksuite/composer/docx.py +542 -292
  9. chgksuite/composer/latex.py +3 -4
  10. chgksuite/composer/lj.py +1 -2
  11. chgksuite/composer/{reddit.py → markdown.py} +35 -25
  12. chgksuite/composer/openquiz.py +2 -3
  13. chgksuite/composer/pptx.py +18 -6
  14. chgksuite/composer/telegram.py +22 -10
  15. chgksuite/handouter/gen.py +11 -7
  16. chgksuite/handouter/installer.py +0 -0
  17. chgksuite/handouter/runner.py +237 -10
  18. chgksuite/handouter/tex_internals.py +12 -13
  19. chgksuite/handouter/utils.py +22 -1
  20. chgksuite/lastdir +1 -0
  21. chgksuite/parser.py +218 -37
  22. chgksuite/parser_db.py +4 -6
  23. chgksuite/resources/labels_az.toml +22 -0
  24. chgksuite/resources/labels_by.toml +1 -2
  25. chgksuite/resources/labels_by_tar.toml +1 -2
  26. chgksuite/resources/labels_en.toml +1 -2
  27. chgksuite/resources/labels_kz_cyr.toml +1 -2
  28. chgksuite/resources/labels_ru.toml +1 -2
  29. chgksuite/resources/labels_sr.toml +1 -2
  30. chgksuite/resources/labels_ua.toml +1 -2
  31. chgksuite/resources/labels_uz.toml +0 -3
  32. chgksuite/resources/labels_uz_cyr.toml +1 -2
  33. chgksuite/resources/regexes_az.json +17 -0
  34. chgksuite/resources/regexes_by.json +3 -2
  35. chgksuite/resources/regexes_by_tar.json +17 -0
  36. chgksuite/resources/regexes_en.json +3 -2
  37. chgksuite/resources/regexes_kz_cyr.json +3 -2
  38. chgksuite/resources/regexes_ru.json +3 -2
  39. chgksuite/resources/regexes_sr.json +3 -2
  40. chgksuite/resources/regexes_ua.json +3 -2
  41. chgksuite/resources/regexes_uz.json +16 -0
  42. chgksuite/resources/regexes_uz_cyr.json +3 -2
  43. chgksuite/trello.py +8 -9
  44. chgksuite/typotools.py +9 -8
  45. chgksuite/version.py +1 -1
  46. {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/METADATA +10 -19
  47. chgksuite-0.27.0.dist-info/RECORD +63 -0
  48. {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/WHEEL +1 -2
  49. chgksuite/composer/telegram_parser.py +0 -230
  50. chgksuite-0.26.0b11.dist-info/RECORD +0 -59
  51. chgksuite-0.26.0b11.dist-info/top_level.txt +0 -1
  52. {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/entry_points.txt +0 -0
  53. {chgksuite-0.26.0b11.dist-info → chgksuite-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -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" "\\vspace{{0.8em}}\n".format(
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" "\\vspace{{0.8em}}\n".format(
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 codecs.open(outfilename, "w", "utf8") as outfile:
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 codecs.open("lj.debug", "w", "utf8") as f:
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 RedditExporter(BaseExporter):
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 reddityapper(self, e):
17
+ def markdownyapper(self, e):
19
18
  if isinstance(e, str):
20
- return self.reddit_element_layout(e)
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.reddit_element_layout(e)
22
+ return self.markdown_element_layout(e)
24
23
  else:
25
- return " \n".join([self.reddit_element_layout(x) for x in e])
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 redditformat(self, s):
39
+ def markdownformat(self, s):
41
40
  res = ""
42
41
  for run in self.parse_4s_elem(s):
43
- if run[0] in ("", "hyperlink"):
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
- res += "[картинка]({})".format(imglink)
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 reddit_element_layout(self, e):
64
+ def markdown_element_layout(self, e):
61
65
  res = ""
62
66
  if isinstance(e, str):
63
- res = self.redditformat(e)
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.reddit_element_layout(x))
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 reddit_format_element(self, pair):
78
+ def markdown_format_element(self, pair):
75
79
  if pair[0] == "Question":
76
- return self.reddit_format_question(pair[1])
80
+ return self.markdown_format_question(pair[1])
77
81
 
78
- def reddit_format_question(self, q):
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.reddityapper(q["question"]),
87
+ self.markdownyapper(q["question"]),
84
88
  )
85
89
  if "number" not in q:
86
90
  self.qcount += 1
87
- res += "__Ответ:__ >!{} \n".format(self.reddityapper(q["answer"]))
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.reddityapper(q["zachet"]))
97
+ res += "__Зачёт:__ {} \n".format(self.markdownyapper(q["zachet"]))
90
98
  if "nezachet" in q:
91
- res += "__Незачёт:__ {} \n".format(self.reddityapper(q["nezachet"]))
99
+ res += "__Незачёт:__ {} \n".format(self.markdownyapper(q["nezachet"]))
92
100
  if "comment" in q:
93
- res += "__Комментарий:__ {} \n".format(self.reddityapper(q["comment"]))
101
+ res += "__Комментарий:__ {} \n".format(self.markdownyapper(q["comment"]))
94
102
  if "source" in q:
95
- res += "__Источник:__ {} \n".format(self.reddityapper(q["source"]))
103
+ res += "__Источник:__ {} \n".format(self.markdownyapper(q["source"]))
96
104
  if "author" in q:
97
- res += "!<\n__Автор:__ {} \n".format(self.reddityapper(q["author"]))
105
+ res += "{}\n__Автор:__ {} \n".format(
106
+ spoiler_end, self.markdownyapper(q["author"])
107
+ )
98
108
  else:
99
- res += "!<\n"
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.reddit_format_element(pair)
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 codecs.open(outfile, "w", "utf8") as f:
119
+ with open(outfile, "w", encoding="utf-8") as f:
110
120
  f.write(text)
111
121
  self.logger.info("Output: {}".format(outfile))
@@ -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.labels["question_labels"]["handout_short"]
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 codecs.open(outfilename, "w", "utf8") as f:
177
+ with open(outfilename, "w", encoding="utf-8") as f:
179
178
  f.write(json.dumps(result, indent=2, ensure_ascii=False))
@@ -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 BaseExporter, backtick_replace, parseimg
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, s, image=None, strip_brackets=True, replace_spaces=True
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.labels["question_labels"]["handout_short"]
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.replace("\u0301", "")
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(self.pptx_process_text(handout), p, tf, slide)
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"])
@@ -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("You should post to the PRIVATE chat, not to the channel/group")
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 | str | None:
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("The bot MUST be added do the group and made admin, else it won't work!")
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 extracted_id == channel_numeric_id or extracted_id == self.control_chat_id:
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 "message" in msg_data and "forward_from_chat" in msg_data["message"]:
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 ((check_type and forward_info.get("type") == "channel") or (
1191
- not check_type
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(
@@ -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
- labels = toml.loads(
57
- read_file(os.path.join(resourcedir, f"labels_{args.lang}.toml"))
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:
File without changes