e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
315 lines
10 KiB
Python
315 lines
10 KiB
Python
"""
|
|
Skill classification taxonomy
|
|
=============================
|
|
|
|
The single Python source of truth for EduHub's skill classification — the
|
|
required ``track`` (one-of) plus the optional faceting dimensions
|
|
(``language``, ``domains``, ``stages``, ``forms``, ``audiences``). It mirrors
|
|
the eduhub web's ``src/data/taxonomy.ts`` (now maintained in zzhtx258/eduhub,
|
|
``packages/eduhub-web``) value-by-value so the interactive
|
|
``skill publish`` / ``skill update`` flows can prompt with the same labels the
|
|
web upload form and ``/api/v1`` validation use.
|
|
|
|
Values are the lowercase tokens the API expects; the ``zh``/``en`` labels are
|
|
display-only. ``domains`` is a two-level tree: a top-level slug (``arts``) or a
|
|
dotted child (``arts.instruments``). Anything finer than a child belongs in
|
|
free-form ``tags``, not here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Option:
|
|
"""One taxonomy choice: the API token plus bilingual display labels."""
|
|
|
|
value: str
|
|
zh: str
|
|
en: str
|
|
|
|
def label(self, locale: str = "zh") -> str:
|
|
return self.en if locale == "en" else self.zh
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DomainNode:
|
|
"""A top-level domain and its dotted children."""
|
|
|
|
value: str
|
|
zh: str
|
|
en: str
|
|
children: tuple[Option, ...] = ()
|
|
|
|
def label(self, locale: str = "zh") -> str:
|
|
return self.en if locale == "en" else self.zh
|
|
|
|
|
|
# ── required: track (single-select) ───────────────────────────────────────
|
|
|
|
TRACK_OPTIONS: tuple[Option, ...] = (
|
|
Option("academics", "学业辅导", "Academics"),
|
|
Option("companions", "成长陪伴", "Companions"),
|
|
Option("skills-interests", "兴趣与技能", "Skills & Interests"),
|
|
Option("educators", "教育者工具", "For Educators"),
|
|
)
|
|
|
|
# ── required: language (single-select, defaults to zh) ─────────────────────
|
|
|
|
LANGUAGE_OPTIONS: tuple[Option, ...] = (
|
|
Option("zh", "中文", "Chinese"),
|
|
Option("en", "英文", "English"),
|
|
Option("ja", "日文", "Japanese"),
|
|
Option("other", "其他", "Other"),
|
|
)
|
|
|
|
# ── optional: stage (multi-select) ─────────────────────────────────────────
|
|
|
|
STAGE_OPTIONS: tuple[Option, ...] = (
|
|
Option("preschool", "学前", "Preschool"),
|
|
Option("primary", "小学", "Primary"),
|
|
Option("junior-high", "初中", "Junior High"),
|
|
Option("senior-high", "高中", "Senior High"),
|
|
Option("university", "大学", "University"),
|
|
Option("adult", "成人", "Adult"),
|
|
)
|
|
|
|
# ── optional: form (multi-select) ──────────────────────────────────────────
|
|
|
|
FORM_OPTIONS: tuple[Option, ...] = (
|
|
Option("tutor", "讲解答疑", "Tutoring"),
|
|
Option("practice", "练习陪练", "Practice"),
|
|
Option("feedback", "批改反馈", "Feedback"),
|
|
Option("assessment", "测评诊断", "Assessment"),
|
|
Option("planning", "规划方法", "Planning"),
|
|
Option("companion", "人格陪伴", "Companion"),
|
|
Option("tool", "实用工具", "Tool"),
|
|
Option("reference", "资料内容", "Reference"),
|
|
)
|
|
|
|
# ── optional: audience (multi-select, empty means learners) ────────────────
|
|
|
|
AUDIENCE_OPTIONS: tuple[Option, ...] = (
|
|
Option("learner", "学习者", "Learners"),
|
|
Option("teacher", "教师", "Teachers"),
|
|
Option("parent", "家长", "Parents"),
|
|
)
|
|
|
|
# ── optional: domains (multi-select, two-level tree) ───────────────────────
|
|
|
|
|
|
def _domain(value: str, zh: str, en: str, children: list[tuple[str, str, str]]) -> DomainNode:
|
|
return DomainNode(
|
|
value=value,
|
|
zh=zh,
|
|
en=en,
|
|
children=tuple(Option(f"{value}.{c}", czh, cen) for c, czh, cen in children),
|
|
)
|
|
|
|
|
|
DOMAIN_TREE: tuple[DomainNode, ...] = (
|
|
_domain(
|
|
"lang-lit",
|
|
"语言与文学",
|
|
"Languages & Literature",
|
|
[
|
|
("chinese", "中文", "Chinese"),
|
|
("english", "英语", "English"),
|
|
("japanese", "日语", "Japanese"),
|
|
("korean", "韩语", "Korean"),
|
|
("french", "法语", "French"),
|
|
("german", "德语", "German"),
|
|
("spanish", "西语", "Spanish"),
|
|
("other-lang", "其他语种", "Other Languages"),
|
|
("reading", "文学与阅读", "Literature & Reading"),
|
|
],
|
|
),
|
|
_domain(
|
|
"math",
|
|
"数学与逻辑",
|
|
"Math & Logic",
|
|
[
|
|
("arithmetic", "启蒙数学", "Early Math"),
|
|
("algebra", "代数", "Algebra"),
|
|
("geometry", "几何", "Geometry"),
|
|
("calculus", "微积分", "Calculus"),
|
|
("statistics", "概率统计", "Statistics"),
|
|
("competition", "竞赛数学", "Competition Math"),
|
|
("logic", "逻辑思维", "Logic"),
|
|
],
|
|
),
|
|
_domain(
|
|
"science",
|
|
"自然科学",
|
|
"Natural Sciences",
|
|
[
|
|
("physics", "物理", "Physics"),
|
|
("chemistry", "化学", "Chemistry"),
|
|
("biology", "生物", "Biology"),
|
|
("earth-astro", "地球与天文", "Earth & Astronomy"),
|
|
],
|
|
),
|
|
_domain(
|
|
"computing",
|
|
"计算机与数据",
|
|
"Computing & Data",
|
|
[
|
|
("programming", "编程入门", "Programming Basics"),
|
|
("software", "软件开发", "Software Development"),
|
|
("data-science", "数据科学", "Data Science"),
|
|
("ai", "人工智能", "AI"),
|
|
("security", "网络安全", "Security"),
|
|
("oi", "信息学竞赛", "Competitive Programming"),
|
|
],
|
|
),
|
|
_domain(
|
|
"humanities",
|
|
"人文社科",
|
|
"Humanities & Social Sciences",
|
|
[
|
|
("history", "历史", "History"),
|
|
("geography", "地理", "Geography"),
|
|
("civics", "政治与公民", "Civics"),
|
|
("philosophy", "哲学", "Philosophy"),
|
|
("psychology", "心理学", "Psychology"),
|
|
("economics", "经济学", "Economics"),
|
|
("law", "法律", "Law"),
|
|
],
|
|
),
|
|
_domain(
|
|
"arts",
|
|
"艺术与创意",
|
|
"Arts & Creativity",
|
|
[
|
|
("music-theory", "乐理", "Music Theory"),
|
|
("instruments", "器乐", "Instruments"),
|
|
("vocal", "声乐", "Vocal"),
|
|
("painting", "绘画", "Painting"),
|
|
("calligraphy", "书法", "Calligraphy"),
|
|
("design", "设计", "Design"),
|
|
("photography", "摄影", "Photography"),
|
|
("creative-writing", "创意写作", "Creative Writing"),
|
|
],
|
|
),
|
|
_domain(
|
|
"general",
|
|
"通识素养",
|
|
"General Skills",
|
|
[
|
|
("speaking", "演讲口才", "Public Speaking"),
|
|
("debate", "辩论思辨", "Debate"),
|
|
("writing", "写作表达", "Writing"),
|
|
("info-literacy", "信息素养", "Information Literacy"),
|
|
("memory", "记忆与速读", "Memory & Speed Reading"),
|
|
],
|
|
),
|
|
_domain(
|
|
"business",
|
|
"商业与职业",
|
|
"Business & Careers",
|
|
[
|
|
("office", "办公技能", "Office Skills"),
|
|
("data-analysis", "数据分析", "Data Analysis"),
|
|
("media", "新媒体运营", "Digital Media"),
|
|
("management", "管理领导力", "Management"),
|
|
("finance", "金融会计", "Finance & Accounting"),
|
|
("career", "求职面试", "Career & Interviews"),
|
|
],
|
|
),
|
|
_domain(
|
|
"life",
|
|
"生活与爱好",
|
|
"Life & Hobbies",
|
|
[
|
|
("cooking", "烹饪美食", "Cooking"),
|
|
("fitness", "运动健身", "Fitness"),
|
|
("crafts", "手工园艺", "Crafts & Gardening"),
|
|
("games", "棋类与游戏", "Board Games"),
|
|
("personal-finance", "个人理财", "Personal Finance"),
|
|
("life-skills", "生活技能", "Life Skills"),
|
|
],
|
|
),
|
|
_domain(
|
|
"wellbeing",
|
|
"身心健康",
|
|
"Mind & Wellbeing",
|
|
[
|
|
("mind", "心理与情绪", "Mental & Emotional"),
|
|
("mindfulness", "正念冥想", "Mindfulness"),
|
|
("parenting", "家庭教育", "Parenting"),
|
|
("communication", "人际沟通", "Communication"),
|
|
],
|
|
),
|
|
_domain(
|
|
"engineering",
|
|
"工程与创客",
|
|
"Engineering & Making",
|
|
[
|
|
("robotics", "机器人", "Robotics"),
|
|
("electronics", "电子电路", "Electronics"),
|
|
("printing-3d", "3D打印与建模", "3D Printing & Modeling"),
|
|
("drones", "无人机与航模", "Drones & Models"),
|
|
],
|
|
),
|
|
)
|
|
|
|
|
|
# ── lookup / validation helpers ────────────────────────────────────────────
|
|
|
|
TRACK_VALUES: frozenset[str] = frozenset(o.value for o in TRACK_OPTIONS)
|
|
LANGUAGE_VALUES: frozenset[str] = frozenset(o.value for o in LANGUAGE_OPTIONS)
|
|
STAGE_VALUES: frozenset[str] = frozenset(o.value for o in STAGE_OPTIONS)
|
|
FORM_VALUES: frozenset[str] = frozenset(o.value for o in FORM_OPTIONS)
|
|
AUDIENCE_VALUES: frozenset[str] = frozenset(o.value for o in AUDIENCE_OPTIONS)
|
|
|
|
DOMAIN_VALUES: frozenset[str] = frozenset(
|
|
[node.value for node in DOMAIN_TREE]
|
|
+ [child.value for node in DOMAIN_TREE for child in node.children]
|
|
)
|
|
|
|
|
|
def is_valid_track(value: str) -> bool:
|
|
return value in TRACK_VALUES
|
|
|
|
|
|
def track_label(value: str, locale: str = "zh") -> str:
|
|
for o in TRACK_OPTIONS:
|
|
if o.value == value:
|
|
return o.label(locale)
|
|
return value
|
|
|
|
|
|
def domain_label(value: str, locale: str = "zh") -> str:
|
|
"""Leaf label: ``arts.instruments`` -> 器乐, ``arts`` -> 艺术与创意."""
|
|
root = value.split(".")[0]
|
|
for node in DOMAIN_TREE:
|
|
if node.value == root:
|
|
if node.value == value:
|
|
return node.label(locale)
|
|
for child in node.children:
|
|
if child.value == value:
|
|
return child.label(locale)
|
|
return value
|
|
|
|
|
|
__all__ = [
|
|
"AUDIENCE_OPTIONS",
|
|
"AUDIENCE_VALUES",
|
|
"DOMAIN_TREE",
|
|
"DOMAIN_VALUES",
|
|
"DomainNode",
|
|
"FORM_OPTIONS",
|
|
"FORM_VALUES",
|
|
"LANGUAGE_OPTIONS",
|
|
"LANGUAGE_VALUES",
|
|
"Option",
|
|
"STAGE_OPTIONS",
|
|
"STAGE_VALUES",
|
|
"TRACK_OPTIONS",
|
|
"TRACK_VALUES",
|
|
"domain_label",
|
|
"is_valid_track",
|
|
"track_label",
|
|
]
|