Skip to content

Commit 2dfcce2

Browse files
committed
feat: add schema template / metaclass support
fix: set default superclass of imported ontology classes to owl:Thing
1 parent 3d03279 commit 2dfcce2

5 files changed

Lines changed: 327 additions & 1 deletion

File tree

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ install_requires =
6767
matplotlib
6868
scipy
6969
dask
70+
pybars3
7071

7172
[options.packages.find]
7273
where = src

src/osw/core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import osw.model.entity as model
1818
from osw.model.static import OswBaseModel
19+
from osw.utils.templates import eval_handlebars_template
1920
from osw.utils.util import parallelize
2021
from osw.utils.wiki import (
2122
get_namespace,
@@ -567,6 +568,7 @@ class StoreEntityParam(model.OswBaseModel):
567568
entities: Union[OswBaseModel, List[OswBaseModel]]
568569
namespace: Optional[str]
569570
parallel: Optional[bool] = False
571+
meta_category_title: Optional[str] = "Category:Category"
570572
debug: Optional[bool] = False
571573

572574
def store_entity(
@@ -590,6 +592,10 @@ def store_entity(
590592
if max_index >= 5:
591593
param.parallel = True
592594

595+
meta_category = self.site.get_page(
596+
WtSite.GetPageParam(titles=[param.meta_category_title])
597+
).pages[0]
598+
593599
def store_entity_(
594600
entity: model.Entity, namespace_: str = None, index: int = None
595601
) -> None:
@@ -614,6 +620,15 @@ def store_entity_(
614620
page.set_slot_content(
615621
"footer", "{{#invoke:Entity|footer}}"
616622
) # required for footer rendering
623+
if namespace_ == "Category":
624+
template = meta_category.get_slot_content("schema_template")
625+
if template:
626+
schema = json.loads(
627+
eval_handlebars_template(
628+
template, jsondata, {"_page_title": entity_title}
629+
)
630+
)
631+
page.set_slot_content("jsonschema", schema)
617632
page.edit()
618633
if index is None:
619634
print(f"Entity stored at {page.get_url()}.")

src/osw/ontology.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ class ImportConfig(OswBaseModel):
7777
base_class_title: Optional[
7878
str
7979
] = "Category:OSW725a3cf5458f4daea86615fcbd0029f8" # OwlClass
80-
"""Title of the base class schema"""
80+
"""Title of the base class schema. Defaults to OwlClass"""
81+
meta_class_title: Optional[
82+
str
83+
] = "Category:OSW379d5a1589c74c82bc0de47938264d00" # OwlThing
84+
"""Title of the meta class schema. Defaults to OwlThing"""
8185
dump_files: Optional[bool] = False
8286
"""If True, the parsed ontology will be dumped to a jsonld file"""
8387
dump_path: Optional[str] = None
@@ -709,6 +713,10 @@ def _store_ontology(self, param: StoreOntologyParam):
709713
# see https://www.semantic-mediawiki.org/wiki/Help:Import_vocabulary
710714
if namespace == "Category":
711715
smw_import_type = "Category"
716+
if not hasattr(e, "subclass_of"):
717+
e.subclass_of = []
718+
if len(e.subclass_of) == 0:
719+
e.subclass_of = self.import_config.meta_class_title
712720
elif namespace == "Property":
713721
smw_import_type = "Type:" + e.cast(model.Property).property_type
714722
else:

src/osw/utils/templates.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pybars import Compiler
2+
3+
4+
def eval_handlebars_template(
5+
template, data, helpers={}, partials={}, add_self_as_partial=True
6+
):
7+
"""evaluates a handlebars template with the given data
8+
9+
Parameters
10+
----------
11+
template
12+
the template string
13+
data
14+
the data dictionary
15+
helpers, optional
16+
helper functions, by default {}
17+
partials, optional
18+
partials, by default {}
19+
add_self_as_partial, optional
20+
if true, add the compiled template as partial 'self', by default True
21+
22+
Returns
23+
-------
24+
the evaluated template as a string
25+
"""
26+
compiler = Compiler()
27+
template = compiler.compile(template)
28+
if add_self_as_partial:
29+
partials["self"] = template
30+
return template(data, helpers=helpers, partials=partials)

tests/utils/templates_test.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import json
2+
3+
from osw.utils.templates import eval_handlebars_template
4+
5+
6+
def test_category_template():
7+
template = """
8+
{
9+
"@context": [
10+
{{#each subclass_of}}
11+
"/wiki/{{{.}}}?action=raw\u0026slot=jsonschema"{{#unless @last}},{{/unless}}
12+
{{/each}}
13+
],
14+
"allOf": [
15+
{
16+
{{#each subclass_of}}
17+
"$ref": "/wiki/{{{.}}}?action=raw\u0026slot=jsonschema"{{#unless @last}},{{/unless}}
18+
{{/each}}
19+
}
20+
],
21+
"type": "object",
22+
"uuid": "{{{uuid}}}",
23+
"title": "{{{name}}}",
24+
"title*": {
25+
{{#each label}}
26+
"{{{lang}}}": "{{{text}}}"{{#unless @last}},{{/unless}}
27+
{{/each}}
28+
},
29+
"description": "{{{description.[0].text}}}",
30+
"description*": {
31+
{{#each description}}
32+
"{{{lang}}}": "{{{text}}}"{{#unless @last}},{{/unless}}
33+
{{/each}}
34+
},
35+
"required": ["type"],
36+
"properties": {
37+
"type": {
38+
"default": ["{{{_page_title}}}"]
39+
}
40+
}
41+
}"""
42+
43+
data = {
44+
"type": ["Category:Category"],
45+
"subclass_of": ["Category:Category"],
46+
"uuid": "379d5a15-89c7-4c82-bc0d-e47938264d00",
47+
"label": [{"text": "OwlThing", "lang": "en"}],
48+
"metaclass": ["Category:OSW725a3cf5458f4daea86615fcbd0029f8"],
49+
"description": [
50+
{
51+
"text": "Represents the set of all individuals. In the DL literature this is often called the top concept.",
52+
"lang": "en",
53+
}
54+
],
55+
"name": "OwlThing",
56+
}
57+
58+
expected = """
59+
{
60+
"@context": [
61+
"/wiki/Category:Category?action=raw&slot=jsonschema"
62+
],
63+
"allOf": [
64+
{
65+
"$ref": "/wiki/Category:Category?action=raw&slot=jsonschema"
66+
}
67+
],
68+
"type": "object",
69+
"uuid": "379d5a15-89c7-4c82-bc0d-e47938264d00",
70+
"title": "OwlThing",
71+
"title*": {
72+
"en": "OwlThing"
73+
},
74+
"description": "Represents the set of all individuals. In the DL literature this is often called the top concept.",
75+
"description*": {
76+
"en": "Represents the set of all individuals. In the DL literature this is often called the top concept."
77+
},
78+
"required": [
79+
"type"
80+
],
81+
"properties": {
82+
"type": {
83+
"default": [
84+
"Category:OSW379d5a1589c74c82bc0de47938264d00"
85+
]
86+
}
87+
}
88+
}
89+
"""
90+
91+
output = json.loads(
92+
eval_handlebars_template(
93+
template,
94+
data,
95+
{"_page_title": "Category:OSW379d5a1589c74c82bc0de47938264d00"},
96+
)
97+
)
98+
99+
assert output == json.loads(expected)
100+
101+
102+
def test_metamodel_template():
103+
template = """
104+
{
105+
"title": "{{{name}}}",
106+
"required": ["uuid"],
107+
"properties": {
108+
"uuid": {
109+
"type": "string",
110+
"format": "uuid",
111+
"options": {
112+
"hidden": true
113+
}
114+
} {{#each parameters}},
115+
"{{{name}}}": {
116+
"title": "{{{name}}}",
117+
"type": "{{{type}}}" {{#if default}},
118+
"default": "{{{default}}}"{{/if}}
119+
} {{/each}}{{#each submodels}},
120+
"{{{name}}}":
121+
{{> self}}
122+
{{/each}}
123+
}
124+
}"""
125+
126+
data = {
127+
"submodels": [
128+
{
129+
"name": "Geometrie",
130+
"parameters": [{"name": "FaceArea"}, {"name": "dimension"}],
131+
"submodels": [
132+
{
133+
"name": "NegativeElectrode",
134+
"parameters": [],
135+
"submodels": [
136+
{
137+
"name": "ActiveMaterial",
138+
"parameters": [{"name": "thickness"}],
139+
"submodels": [],
140+
}
141+
],
142+
},
143+
{
144+
"name": "PositiveElectrode",
145+
"parameters": [],
146+
"submodels": [
147+
{
148+
"name": "ActiveMaterial",
149+
"parameters": [{"name": "thickness"}],
150+
"submodels": [],
151+
}
152+
],
153+
},
154+
],
155+
}
156+
],
157+
"type": ["Category:OSWecff4345b4b049218f8d6628dc2f2f21"],
158+
"uuid": "dab254dd-1006-4ddf-9554-64b7a5baf332",
159+
"name": "BattmoModel",
160+
"label": [{"text": "BattMo Model", "lang": "en"}],
161+
}
162+
163+
expected = """{
164+
"title":"BattmoModel",
165+
"required":[
166+
"uuid"
167+
],
168+
"properties":{
169+
"uuid":{
170+
"type":"string",
171+
"format":"uuid",
172+
"options":{
173+
"hidden":true
174+
}
175+
},
176+
"Geometrie":{
177+
"title":"Geometrie",
178+
"required":[
179+
"uuid"
180+
],
181+
"properties":{
182+
"uuid":{
183+
"type":"string",
184+
"format":"uuid",
185+
"options":{
186+
"hidden":true
187+
}
188+
},
189+
"FaceArea":{
190+
"title":"FaceArea",
191+
"type":""
192+
},
193+
"dimension":{
194+
"title":"dimension",
195+
"type":""
196+
},
197+
"NegativeElectrode":{
198+
"title":"NegativeElectrode",
199+
"required":[
200+
"uuid"
201+
],
202+
"properties":{
203+
"uuid":{
204+
"type":"string",
205+
"format":"uuid",
206+
"options":{
207+
"hidden":true
208+
}
209+
},
210+
"ActiveMaterial":{
211+
"title":"ActiveMaterial",
212+
"required":[
213+
"uuid"
214+
],
215+
"properties":{
216+
"uuid":{
217+
"type":"string",
218+
"format":"uuid",
219+
"options":{
220+
"hidden":true
221+
}
222+
},
223+
"thickness":{
224+
"title":"thickness",
225+
"type":""
226+
}
227+
}
228+
}
229+
}
230+
},
231+
"PositiveElectrode":{
232+
"title":"PositiveElectrode",
233+
"required":[
234+
"uuid"
235+
],
236+
"properties":{
237+
"uuid":{
238+
"type":"string",
239+
"format":"uuid",
240+
"options":{
241+
"hidden":true
242+
}
243+
},
244+
"ActiveMaterial":{
245+
"title":"ActiveMaterial",
246+
"required":[
247+
"uuid"
248+
],
249+
"properties":{
250+
"uuid":{
251+
"type":"string",
252+
"format":"uuid",
253+
"options":{
254+
"hidden":true
255+
}
256+
},
257+
"thickness":{
258+
"title":"thickness",
259+
"type":""
260+
}
261+
}
262+
}
263+
}
264+
}
265+
}
266+
}
267+
}
268+
}"""
269+
270+
output = json.loads(eval_handlebars_template(template, data))
271+
272+
assert output == json.loads(expected)

0 commit comments

Comments
 (0)