This commit is contained in:
Hartmut Seichter 2025-03-18 19:11:56 +01:00
parent 2e1490d98f
commit 1d00963035
4 changed files with 411 additions and 361 deletions

View file

@ -10,10 +10,6 @@ import yaml
from coursebuilder.schema import Schema from coursebuilder.schema import Schema
class LanguageSelector:
context_lang: str
class Course: class Course:
i18n_name: str = "lang.{}.yaml" i18n_name: str = "lang.{}.yaml"
mod_name: str = "mod.yaml" mod_name: str = "mod.yaml"
@ -28,19 +24,19 @@ class Course:
for p in path.parent.glob(Course.i18n_name.format("*")) for p in path.parent.glob(Course.i18n_name.format("*"))
} }
def validate(self, *, schema: Schema, lang: str) -> None:
print(self.__data)
pass
def __getitem__(self, name: str, /) -> Any: def __getitem__(self, name: str, /) -> Any:
return ( """this overload magic function takes a key and a language code like my_key.en or my_key.de"""
self.__data[name] s = name.split(".")
if name in self.__data.keys()
else self.__i18n[LanguageSelector.context_lang][name]
)
def __getattr__(self, name: str, /) -> Any: if len(s) != 2:
return self.__data[name] raise ValueError(
"query with item selector requires form ['key.lang'] alternative is ['key.*']"
)
return (
self.__i18n[s[-1]][s[0]]
if s[-1] in self.__i18n.keys() and s[0] in self.__i18n[s[-1]].keys()
else self.__data[s[0]]
)
def __str__(self): def __str__(self):
return f"data:{self.__data}\ni18n:{self.__i18n}" return f"data:{self.__data}\ni18n:{self.__i18n}"
@ -83,22 +79,52 @@ def main():
type=str, type=str,
help="schema to validate against", help="schema to validate against",
) )
parser.add_argument(
"-f",
"--fields",
help="Fields to be used, the table will be build accordingly",
action="extend",
nargs="+",
type=str,
)
parser.add_argument(
"-c",
"--create",
help="Fields to be used, the table will be build accordingly",
type=str,
)
# get arguments # get arguments
args = parser.parse_args() args = parser.parse_args()
# just input # with input
if args.input and args.schema: if args.input and args.schema:
with open(args.schema) as f_schema: with open(args.schema) as f_schema:
schema = Schema(schema=yaml.load(f_schema, Loader=yaml.Loader)) schema = Schema(schema=yaml.load(f_schema, Loader=yaml.Loader)) # Schema
sc = StudyCourse(path=(Path(".") / args.input).absolute()) sc = StudyCourse(path=(Path(".") / args.input).absolute()) # Database
LanguageSelector.context_lang = "de" actual_fields = args.fields if args.fields else schema.keys()
for k in schema.keys(): for field in actual_fields:
print(k) for lang in sc.languages:
for shortcode, course in sc.courses.items(): for shortcode, course in sc.courses.items():
print(course[k]) print(
field,
"@",
shortcode,
".",
lang,
"\n",
course[f"{field}.{lang}"],
)
elif args.schema and args.create:
schema = Schema(
schema=yaml.load(open(args.schema), Loader=yaml.Loader)
) # Schema
print(schema.fields())
print(schema.types())
print(schema.facets())
# run as main # run as main

View file

@ -1,142 +1,159 @@
class Schema: class Schema:
__yes_vals: list[str] = ["true", "yes", "on"]
__unique: str = "unique"
__spec: str = "spec"
__facets: str = "facets"
__fields: str = "fields"
__type: str = "type"
__label: str = "label"
def __init__(self, *, schema: dict) -> None: def __init__(self, *, schema: dict) -> None:
self.__schema = schema self.__schema = schema
def __getitem__(self, field): def facets(self) -> list[str]:
return self.__schema[field] return list(self.__schema[Schema.__facets])
def keys(self): def fields(self) -> list[str]:
return self.__schema.keys() return list(self.__schema[Schema.__fields].keys())
def is_translatable(self, field): def types(self) -> dict:
if "translatable" in self.__schema[field]: return {
return self.__schema[field]["translatable"] field: a[Schema.__type]
else: for field, a in self.__schema[Schema.__fields].items()
return True }
def needs_spec(self, field): def is_unique(self, field: str) -> bool:
if "spec" in self.__schema[field]:
return self.__schema[field]
else:
return False
def get_value(self, meta: dict, field: str, lang: str):
"""
treats receiving the value like a variant,
returns values with their language specific representations
"""
match self.__schema[field]["type"]:
case "str":
return ( return (
meta[field][lang] Schema.__yes_vals in self.__schema[field][Schema.__unique].lower()
if self.is_translatable(field) if Schema.__unique in self.__schema[field].keys()
else meta[field]["value"] else True
) )
case "enum" | "int" | "num" | "multikey":
return meta[field]["value"] def needs_spec(self, field: str) -> bool:
case "multinum":
return ( return (
meta[field]["value"] Schema.__yes_vals in self.__schema[field][Schema.__spec].lower()
if hasattr(meta[field]["value"], "__iter__") if Schema.__spec in self.__schema[field].keys()
else [ else False
meta[field]["value"],
]
) # force list!
def to_list_of_dict(self, meta, fields, lang):
"""
generates a list of dict which can easily be converted
to a pandas dataframe
"""
# list comprehension for rows
return [
{
"field": field, # field name
"lang": lang, # language shortcode
"type": self.__schema[field]["type"], # datatype
"label": self.__schema[field]["label"][lang], # label
"value": self.get_value(meta, field, lang), # actual value
"template": self.__schema[field]["template"][lang]
if "template" in self.__schema[field]
else None,
# getting crazy with nested dict comprehension
"enum_values": {
k: v[lang] for (k, v) in self.__schema[field]["values"].items()
}
if "enum" in self.__schema[field]["type"]
else None,
"key_values": {
k: v[lang] for (k, v) in self.__schema[field]["keys"].items()
}
if "multikey" in self.__schema[field]["type"]
else None,
"spec": meta[field]["spec"][lang] if "spec" in meta[field] else None,
}
for field in fields
]
def to_short_dict(self, meta, fields, lang):
"""
generates a short version of dict which can easily be converted
to a pandas dataframe
"""
# dict comprehension for whole meta part
return {field: self.get_value(meta, field, lang) for field in fields}
def to_list_of_tuple(self, meta, fields, lang):
"""
generates a list of tuples with a label and value (text)
this is usually consumed by a Markdown generator
todo: needs deuglyfication of free standing loop, templates are possible for all
"""
list = []
for r in self.to_list_of_dict(meta, fields, lang):
match r["type"]:
case "str":
list.append((r["label"], r["value"]))
case "int" | "num":
list.append(
(
r["label"],
r["template"].format(value=r["value"], spec=r["spec"])
if r["template"]
else r["value"],
)
)
case "enum":
list.append(
(
r["label"],
r["template"].format(
value=r["enum_values"][r["value"]], spec=r["spec"]
)
if r["template"]
else r["enum_values"][r["value"]],
)
)
case "multikey":
list.append(
(
r["label"],
", ".join(
[
r["template"].format(
key=r["key_values"][k], value=v
)
for k, v in r["value"].items()
]
),
)
)
case "multinum":
list.append(
(
r["label"],
", ".join(
r["template"].format(value=v) for v in r["value"]
),
)
) )
return list # def get_value(self, meta: dict, field: str, lang: str):
# """
# treats receiving the value like a variant,
# returns values with their language specific representations
# """
# match self.__schema[field]["type"]:
# case "str":
# return (
# meta[field][lang]
# if self.is_translatable(field)
# else meta[field]["value"]
# )
# case "enum" | "int" | "num" | "multikey":
# return meta[field]["value"]
# case "multinum":
# return (
# meta[field]["value"]
# if hasattr(meta[field]["value"], "__iter__")
# else [
# meta[field]["value"],
# ]
# ) # force list!
# def to_list_of_dict(self, meta, fields, lang):
# """
# generates a list of dict which can easily be converted
# to a pandas dataframe
# """
# # list comprehension for rows
# return [
# {
# "field": field, # field name
# "lang": lang, # language shortcode
# "type": self.__schema[field]["type"], # datatype
# "label": self.__schema[field]["label"][lang], # label
# "value": self.get_value(meta, field, lang), # actual value
# "template": self.__schema[field]["template"][lang]
# if "template" in self.__schema[field]
# else None,
# # getting crazy with nested dict comprehension
# "enum_values": {
# k: v[lang] for (k, v) in self.__schema[field]["values"].items()
# }
# if "enum" in self.__schema[field]["type"]
# else None,
# "key_values": {
# k: v[lang] for (k, v) in self.__schema[field]["keys"].items()
# }
# if "multikey" in self.__schema[field]["type"]
# else None,
# "spec": meta[field]["spec"][lang] if "spec" in meta[field] else None,
# }
# for field in fields
# ]
# def to_short_dict(self, meta, fields, lang):
# """
# generates a short version of dict which can easily be converted
# to a pandas dataframe
# """
# # dict comprehension for whole meta part
# return {field: self.get_value(meta, field, lang) for field in fields}
# def to_list_of_tuple(self, meta, fields, lang):
# """
# generates a list of tuples with a label and value (text)
# this is usually consumed by a Markdown generator
# todo: needs deuglyfication of free standing loop, templates are possible for all
# """
# list = []
# for r in self.to_list_of_dict(meta, fields, lang):
# match r["type"]:
# case "str":
# list.append((r["label"], r["value"]))
# case "int" | "num":
# list.append(
# (
# r["label"],
# r["template"].format(value=r["value"], spec=r["spec"])
# if r["template"]
# else r["value"],
# )
# )
# case "enum":
# list.append(
# (
# r["label"],
# r["template"].format(
# value=r["enum_values"][r["value"]], spec=r["spec"]
# )
# if r["template"]
# else r["enum_values"][r["value"]],
# )
# )
# case "multikey":
# list.append(
# (
# r["label"],
# ", ".join(
# [
# r["template"].format(
# key=r["key_values"][k], value=v
# )
# for k, v in r["value"].items()
# ]
# ),
# )
# )
# case "multinum":
# list.append(
# (
# r["label"],
# ", ".join(
# r["template"].format(value=v) for v in r["value"]
# ),
# )
# )
# return list

View file

@ -13,8 +13,7 @@ credits: 5
form-of-exam: form-of-exam:
type: alternative type: alternative
duration: duration: 1
value: 1
author-of-indenture: author-of-indenture:

View file

@ -1,6 +1,18 @@
# fields in curricular description facets: ["de", "en"]
# leaning on methods in OpenAPI 3.0
templates:
name:
de: Modulname {value}
en: name of course {value}
fields:
#
# Kürzel / ID
#
id:
type: str
translatable: false
label: { de: "Kürzel", en: "code" }
# #
# Modulname # Modulname
# #
@ -19,14 +31,6 @@ instructor:
de: "Modulverantwortlicher / Modulverantwortliche" de: "Modulverantwortlicher / Modulverantwortliche"
en: "module instructor" en: "module instructor"
#
# Kürzel / ID
#
id:
type: str
translatable: false
label: { de: "Kürzel", en: "code" }
# #
# Qualifikationsziele # Qualifikationsziele
# #
@ -77,10 +81,14 @@ form-of-instruction:
{ {
"lecture": { de: "Vorlesung", en: "lecture" }, "lecture": { de: "Vorlesung", en: "lecture" },
"lecture_seminar": "lecture_seminar":
{ de: "Seminaristische Vorlesung", en: "lecture and seminar" }, {
de: "Seminaristische Vorlesung",
en: "lecture and seminar",
},
"seminar": { de: "Seminar", en: "seminar" }, "seminar": { de: "Seminar", en: "seminar" },
"exersise": { de: "Übung", en: "lab exersise" }, "exersise": { de: "Übung", en: "lab exersise" },
"pc_lab": { de: "Rechnergestütztes Praktikum", en: "PC exersise" }, "pc_lab":
{ de: "Rechnergestütztes Praktikum", en: "PC exersise" },
"project": { de: "Project", en: "project" }, "project": { de: "Project", en: "project" },
} }
template: template:
@ -210,18 +218,18 @@ form-of-exam:
# #
# Art der Veranstaltung # Art der Veranstaltung
# #
kind: # kind:
type: enum # type: enum
label: # label:
{ # {
de: "Art der Veranstaltung (Pflicht, Wahl, etc.)", # de: "Art der Veranstaltung (Pflicht, Wahl, etc.)",
en: "kind of module (compulsory, elective)", # en: "kind of module (compulsory, elective)",
} # }
values: # values:
{ # {
"compulsory": { de: "Pflicht", en: "compulsory" }, # "compulsory": { de: "Pflicht", en: "compulsory" },
"elective": { de: "Wahl/Wahlpflicht", en: "elective" }, # "elective": { de: "Wahl/Wahlpflicht", en: "elective" },
} # }
# #
# Freiform Bemerkungen # Freiform Bemerkungen