tree.py 12.8 KB
Newer Older
F
feilong 已提交
1
import json
M
Mars Liu 已提交
2
import logging
F
feilong 已提交
3 4
import os
import re
M
Mars Liu 已提交
5 6
import sys
import uuid
7

M
Mars Liu 已提交
8
import git
F
feilong 已提交
9

M
Mars Liu 已提交
10
id_set = set()
M
Mars Liu 已提交
11
logger = logging.getLogger(__name__)
M
Mars Liu 已提交
12 13 14 15 16
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
M
Mars Liu 已提交
17
repo = git.Repo(".")
M
Mars Liu 已提交
18

19

M
Mars Liu 已提交
20 21
def user_name():
    return repo.config_reader().get_value("user", "name")
22

23 24 25 26 27 28 29 30 31

_DEFAULT_ENCODING = 'utf-8'


def read_text(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        return f.read()


F
feilong 已提交
32
def load_json(p):
33
    return json.loads(read_text(p))
F
feilong 已提交
34 35 36 37 38 39 40 41


def dump_json(p, j, exist_ok=False, override=False):
    if os.path.exists(p):
        if exist_ok:
            if not override:
                return
        else:
M
Mars Liu 已提交
42
            logger.error(f"{p} already exist")
F
feilong 已提交
43 44
            sys.exit(0)

45
    with open(p, 'w+', encoding="utf8") as f:
F
feilong 已提交
46 47 48
        f.write(json.dumps(j, indent=2, ensure_ascii=False))


M
Mars Liu 已提交
49 50 51 52 53 54 55 56 57 58
def ensure_config(path):
    config_path = os.path.join(path, "config.json")
    if not os.path.exists(config_path):
        node = {"keywords": []}
        dump_json(config_path, node, exist_ok=True, override=False)
        return node
    else:
        return load_json(config_path)


F
feilong 已提交
59 60 61 62 63 64 65 66 67 68 69 70
def parse_no_name(d):
    p = r'(\d+)\.(.*)'
    m = re.search(p, d)

    try:
        no = int(m.group(1))
        dir_name = m.group(2)
    except:
        sys.exit(0)

    return no, dir_name

M
Mars Liu 已提交
71

M
Mars Liu 已提交
72 73 74 75 76 77 78 79 80 81 82 83 84
def check_export(base, cfg):
    flag = False
    exports = []
    for export in cfg.get('export', []):
        ecfg_path = os.path.join(base, export)
        if os.path.exists(ecfg_path):
            exports.append(export)
        else:
            flag = True
    if flag:
        cfg["export"] = exports
    return flag

F
feilong 已提交
85

M
Mars Liu 已提交
86
class TreeWalker:
M
Mars Liu 已提交
87
    def __init__(self, root, tree_name, title=None, log=None):
M
Mars Liu 已提交
88 89 90 91
        self.name = tree_name
        self.root = root
        self.title = tree_name if title is None else title
        self.tree = {}
M
Mars Liu 已提交
92
        self.logger = logger if log is None else log
M
Mars Liu 已提交
93

M
Mars Liu 已提交
94 95 96 97 98 99 100 101 102 103 104 105 106
    def walk(self):
        root = self.load_root()
        root_node = {
            "node_id": root["node_id"],
            "keywords": root["keywords"],
            "children": []
        }
        self.tree[root["tree_name"]] = root_node
        self.load_levels(root_node)
        self.load_chapters(self.root, root_node)
        for index, level in enumerate(root_node["children"]):
            level_title = list(level.keys())[0]
            level_node = list(level.values())[0]
107
            level_path = os.path.join(self.root, f"{index + 1}.{level_title}")
M
Mars Liu 已提交
108 109 110 111
            self.load_chapters(level_path, level_node)
            for index, chapter in enumerate(level_node["children"]):
                chapter_title = list(chapter.keys())[0]
                chapter_node = list(chapter.values())[0]
112
                chapter_path = os.path.join(level_path, f"{index + 1}.{chapter_title}")
M
Mars Liu 已提交
113 114 115
                self.load_sections(chapter_path, chapter_node)
                for index, section_node in enumerate(chapter_node["children"]):
                    section_title = list(section_node.keys())[0]
116
                    full_path = os.path.join(chapter_path, f"{index + 1}.{section_title}")
M
Mars Liu 已提交
117
                    if os.path.isdir(full_path):
M
Mars Liu 已提交
118
                        self.check_section_keywords(full_path)
M
Mars Liu 已提交
119 120 121 122 123 124
                        self.ensure_exercises(full_path)

        tree_path = os.path.join(self.root, "tree.json")
        dump_json(tree_path, self.tree, exist_ok=True, override=True)
        return self.tree

125 126 127 128 129
    def sort_dir_list(self, dirs):
        result = [self.extract_node_env(dir) for dir in dirs]
        result.sort(key=lambda item: item[0])
        return result

M
Mars Liu 已提交
130 131 132 133 134 135 136 137
    def load_levels(self, root_node):
        levels = []
        for level in os.listdir(self.root):
            if not os.path.isdir(level):
                continue
            level_path = os.path.join(self.root, level)
            num, config = self.load_level_node(level_path)
            levels.append((num, config))
M
Mars Liu 已提交
138 139

        levels = self.resort_children(self.root, levels)
M
Mars Liu 已提交
140 141 142 143 144 145 146 147 148 149 150 151 152 153
        root_node["children"] = [item[1] for item in levels]
        return root_node

    def load_level_node(self, level_path):
        config = self.ensure_level_config(level_path)
        num, name = self.extract_node_env(level_path)

        result = {
            name: {
                "node_id": config["node_id"],
                "keywords": config["keywords"],
                "children": [],
            }
        }
M
Mars Liu 已提交
154

M
Mars Liu 已提交
155 156 157 158 159 160 161 162 163 164
        return num, result

    def load_chapters(self, base, level_node):
        chapters = []
        for name in os.listdir(base):
            full_name = os.path.join(base, name)
            if os.path.isdir(full_name):
                num, chapter = self.load_chapter_node(full_name)
                chapters.append((num, chapter))

M
Mars Liu 已提交
165
        chapters = self.resort_children(base, chapters)
M
Mars Liu 已提交
166 167 168 169 170 171 172 173 174 175 176
        level_node["children"] = [item[1] for item in chapters]
        return level_node

    def load_sections(self, base, chapter_node):
        sections = []
        for name in os.listdir(base):
            full_name = os.path.join(base, name)
            if os.path.isdir(full_name):
                num, section = self.load_section_node(full_name)
                sections.append((num, section))

M
Mars Liu 已提交
177
        sections = self.resort_children(base, sections)
M
Mars Liu 已提交
178 179 180
        chapter_node["children"] = [item[1] for item in sections]
        return chapter_node

M
Mars Liu 已提交
181 182 183 184 185
    def resort_children(self, base, children):
        children.sort(key=lambda item: item[0])
        for index, [number, element] in enumerate(children):
            title = list(element.keys())[0]
            origin = os.path.join(base, f"{number}.{title}")
186
            posted = os.path.join(base, f"{index + 1}.{title}")
M
Mars Liu 已提交
187 188
            if origin != posted:
                self.logger.info(f"rename [{origin}] to [{posted}]")
189
                os.rename(origin, posted)
M
Mars Liu 已提交
190 191
        return children

M
Mars Liu 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
    def ensure_chapters(self):
        for subdir in os.listdir(self.root):
            self.ensure_level_config(subdir)

    def load_root(self):
        config_path = os.path.join(self.root, "config.json")
        if not os.path.exists(config_path):
            config = {
                "tree_name": self.name,
                "keywords": [],
                "node_id": self.gen_node_id(),
            }
            dump_json(config_path, config, exist_ok=True, override=True)
        else:
            config = load_json(config_path)
            flag, result = self.ensure_node_id(config)
            if flag:
                dump_json(config_path, result, exist_ok=True, override=True)

        return config

    def ensure_level_config(self, path):
        config_path = os.path.join(path, "config.json")
        if not os.path.exists(config_path):
            config = {
                "node_id": self.gen_node_id()
            }
M
Mars Liu 已提交
219
            dump_json(config_path, config, exist_ok=True, override=True)
M
Mars Liu 已提交
220 221 222 223
        else:
            config = load_json(config_path)
            flag, result = self.ensure_node_id(config)
            if flag:
M
Mars Liu 已提交
224
                dump_json(config_path, config, exist_ok=True, override=True)
M
Mars Liu 已提交
225 226 227 228 229 230 231 232 233
        return config

    def ensure_chapter_config(self, path):
        config_path = os.path.join(path, "config.json")
        if not os.path.exists(config_path):
            config = {
                "node_id": self.gen_node_id(),
                "keywords": []
            }
M
Mars Liu 已提交
234
            dump_json(config_path, config, exist_ok=True, override=True)
M
Mars Liu 已提交
235 236 237 238
        else:
            config = load_json(config_path)
            flag, result = self.ensure_node_id(config)
            if flag:
M
Mars Liu 已提交
239
                dump_json(config_path, config, exist_ok=True, override=True)
M
Mars Liu 已提交
240 241 242 243 244 245 246 247
        return config

    def ensure_section_config(self, path):
        config_path = os.path.join(path, "config.json")
        if not os.path.exists(config_path):
            config = {
                "node_id": self.gen_node_id(),
                "keywords": [],
248 249
                "children": [],
                "export": []
M
Mars Liu 已提交
250 251 252 253 254 255
            }
            dump_json(config_path, config, exist_ok=True, override=True)
        else:
            config = load_json(config_path)
            flag, result = self.ensure_node_id(config)
            if flag:
256
                dump_json(config_path, result, exist_ok=True, override=True)
M
Mars Liu 已提交
257 258 259
        return config

    def ensure_node_id(self, config):
260
        flag = False
M
Mars Liu 已提交
261
        if "node_id" not in config or \
M
Mars Liu 已提交
262
                not config["node_id"].startswith(f"{self.name}-") or \
M
Mars Liu 已提交
263 264 265 266
                config["node_id"] in id_set:
            new_id = self.gen_node_id()
            id_set.add(new_id)
            config["node_id"] = new_id
267 268 269 270 271 272 273 274
            flag = True

        for child in config.get("children", []):
            child_node = list(child.values())[0]
            f, _ = self.ensure_node_id(child_node)
            flag = flag or f

        return flag, config
M
Mars Liu 已提交
275 276 277 278 279

    def gen_node_id(self):
        return f"{self.name}-{uuid.uuid4().hex}"

    def extract_node_env(self, path):
M
Mars Liu 已提交
280 281 282 283 284 285 286
        try:
            _, dir = os.path.split(path)
            self.logger.info(path)
            number, title = dir.split(".", 1)
            return int(number), title
        except Exception as error:
            self.logger.error(f"目录 [{path}] 解析失败,结构不合法,可能是缺少序号")
287 288
            # sys.exit(1)
            raise error
M
Mars Liu 已提交
289 290 291 292 293 294 295 296 297 298

    def load_chapter_node(self, full_name):
        config = self.ensure_chapter_config(full_name)
        num, name = self.extract_node_env(full_name)
        result = {
            name: {
                "node_id": config["node_id"],
                "keywords": config["keywords"],
                "children": [],
            }
F
feilong 已提交
299
        }
M
Mars Liu 已提交
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
        return num, result

    def load_section_node(self, full_name):
        config = self.ensure_section_config(full_name)
        num, name = self.extract_node_env(full_name)
        result = {
            name: {
                "node_id": config["node_id"],
                "keywords": config["keywords"],
                "children": config.get("children", [])
            }
        }
        # if "children" in config:
        #     result["children"] = config["children"]
        return num, result

    def ensure_exercises(self, section_path):
        config = self.ensure_section_config(section_path)
M
Mars Liu 已提交
318
        flag = False
M
Mars Liu 已提交
319 320 321 322 323
        for e in os.listdir(section_path):
            base, ext = os.path.splitext(e)
            _, source = os.path.split(e)
            if ext != ".md":
                continue
M
Mars Liu 已提交
324 325
            mfile = base + ".json"
            meta_path = os.path.join(section_path, mfile)
M
Mars Liu 已提交
326
            self.ensure_exercises_meta(meta_path, source)
M
Mars Liu 已提交
327
            export = config.get("export", [])
M
Mars Liu 已提交
328
            if mfile not in export and self.name != "algorithm":
M
Mars Liu 已提交
329 330 331 332 333 334
                export.append(mfile)
                flag = True
                config["export"] = export

        if flag:
            dump_json(os.path.join(section_path, "config.json"), config, True, True)
M
Mars Liu 已提交
335

M
Mars Liu 已提交
336 337 338
        for e in config.get("export", []):
            full_name = os.path.join(section_path, e)
            exercise = load_json(full_name)
M
Mars Liu 已提交
339
            if not exercise.get("exercise_id") or exercise.get("exercise_id") in id_set:
M
Mars Liu 已提交
340 341
                eid = uuid.uuid4().hex
                exercise["exercise_id"] = eid
M
Mars Liu 已提交
342
                dump_json(full_name, exercise, True, True)
M
Mars Liu 已提交
343 344
            else:
                id_set.add(exercise["exercise_id"])
M
Mars Liu 已提交
345

M
Mars Liu 已提交
346 347
    def ensure_exercises_meta(self, meta_path, source):
        _, mfile = os.path.split(meta_path)
M
Mars Liu 已提交
348
        meta = None
M
Mars Liu 已提交
349
        if os.path.exists(meta_path):
350
            content = read_text(meta_path)
M
Mars Liu 已提交
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
            if content:
                meta = json.loads(content)
                if "exercise_id" not in meta:
                    meta["exercise_id"] = uuid.uuid4().hex
                if "notebook_enable" not in meta:
                    meta["notebook_enable"] = self.default_notebook()
                if "source" not in meta:
                    meta["source"] = source
                if "author" not in meta:
                    meta["author"] = user_name()
                if "type" not in meta:
                    meta["type"] = "code_options"
            if meta is None:
                meta = {
                    "type": "code_options",
                    "author": user_name(),
                    "source": source,
                    "notebook_enable": self.default_notebook(),
                    "exercise_id": uuid.uuid4().hex
                }
M
Mars Liu 已提交
371 372 373
        dump_json(meta_path, meta, True, True)

    def default_notebook(self):
M
Mars Liu 已提交
374
        if self.name in ["python", "java", "c"]:
M
Mars Liu 已提交
375 376 377 378
            return True
        else:
            return False

M
Mars Liu 已提交
379 380 381 382 383
    def check_section_keywords(self, full_path):
        config = self.ensure_section_config(full_path)
        if not config.get("keywords", []):
            self.logger.error(f"节点 [{full_path}] 的关键字为空,请修改配置文件写入关键字")
            sys.exit(1)