Skip to content

Commit b61f085

Browse files
committed
Add model types implementation from #288
1 parent e77b0ec commit b61f085

22 files changed

+89582
-0
lines changed

src/hermes/model/types/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-FileCopyrightText: 2025 German Aerospace Center (DLR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Meinel
6+
7+
from datetime import date, time, datetime
8+
9+
from .ld_container import ld_container
10+
from .ld_list import ld_list
11+
from .ld_dict import ld_dict
12+
from .ld_context import iri_map
13+
from .pyld_util import JsonLdProcessor
14+
15+
16+
_TYPEMAP = [
17+
# Conversion routines for ld_container
18+
(
19+
lambda c: isinstance(c, ld_container),
20+
{
21+
"ld_container": lambda c, **_: c,
22+
23+
"json": lambda c, **_: c.compact(),
24+
"expanded_json": lambda c, **_: c.ld_value,
25+
}
26+
),
27+
28+
# Wrap expanded_json to ld_container
29+
(ld_container.is_ld_id, dict(python=lambda c, **_: c[0]['@id'])),
30+
(ld_container.is_typed_ld_value, dict(python=ld_container.typed_ld_to_py)),
31+
(ld_container.is_ld_value, dict(python=lambda c, **_: c[0]['@value'])),
32+
(ld_list.is_ld_list, dict(ld_container=ld_list)),
33+
(ld_dict.is_ld_dict, dict(ld_container=ld_dict)),
34+
35+
# Expand and access JSON data
36+
(ld_container.is_json_id, dict(python=lambda c: c["@id"], expanded_json=lambda c, **_: [c])),
37+
(ld_container.is_typed_json_value, dict(python=ld_container.typed_ld_to_py)),
38+
(ld_container.is_json_value, dict(python=lambda c, **_: c["@value"], expanded_json=lambda c, **_: [c])),
39+
(ld_list.is_container, dict(ld_container=lambda c, **kw: ld_list([c], **kw))),
40+
(ld_dict.is_json_dict, dict(ld_container=ld_dict.from_dict)),
41+
42+
(lambda c: isinstance(c, list), dict(ld_container=ld_list.from_list)),
43+
44+
# Wrap internal data types
45+
(lambda v: isinstance(v, (int, float, str, bool)), dict(expanded_json=lambda v, **_: [{"@value": v}])),
46+
47+
(lambda v: isinstance(v, datetime),
48+
dict(expanded_json=lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:DateTime"]}])),
49+
(lambda v: isinstance(v, date),
50+
dict(expanded_json=lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:Date"]}])),
51+
(lambda v: isinstance(v, time),
52+
dict(expanded_json=lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:Time"]}])),
53+
]
54+
55+
56+
def init_typemap():
57+
for typecheck, conversions in _TYPEMAP:
58+
JsonLdProcessor.register_typemap(typecheck, **conversions)
59+
60+
61+
init_typemap()
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# SPDX-FileCopyrightText: 2025 German Aerospace Center (DLR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Meinel
6+
7+
from .pyld_util import JsonLdProcessor, bundled_loader
8+
9+
10+
class ld_container:
11+
"""
12+
Base class for Linked Data containers.
13+
14+
A linked data container impelements a view on the expanded form of an JSON-LD document.
15+
It allows to easily interacts them by hinding all the nesting and automatically mapping
16+
between different forms.
17+
"""
18+
19+
ld_proc = JsonLdProcessor()
20+
21+
def __init__(self, data, *, parent=None, key=None, index=None, context=None):
22+
"""
23+
Create a new instance of an ld_container.
24+
25+
:param data: The expanded json-ld data that is mapped.
26+
:param parent: Optional parent node of this container.
27+
:param key: Optional key into the parent container.
28+
:param context: Optional local context for this container.
29+
"""
30+
31+
# Store basic data
32+
self.parent = parent
33+
self.key = key
34+
self.index = index
35+
self._data = data
36+
37+
self.context = context or []
38+
39+
# Create active context (to use with pyld) depending on the initial variables
40+
# Re-use active context from parent if available
41+
if self.parent:
42+
if self.context:
43+
self.active_ctx = self.ld_proc.process_context(
44+
self.parent.active_ctx,
45+
self.context,
46+
{"documentLoader": bundled_loader})
47+
else:
48+
self.active_ctx = parent.active_ctx
49+
else:
50+
self.active_ctx = self.ld_proc.inital_ctx(
51+
self.full_context,
52+
{"documentLoader": bundled_loader}
53+
)
54+
55+
def add_context(self, context):
56+
self.context = self.merge_to_list(self.context, context)
57+
self.active_ctx = self.ld_proc.process_context(
58+
self.active_ctx,
59+
context,
60+
{"documentLoader": bundled_loader}
61+
)
62+
63+
@property
64+
def full_context(self):
65+
if self.parent is not None:
66+
return self.merge_to_list(self.parent.full_context, self.context)
67+
else:
68+
return self.context
69+
70+
@property
71+
def path(self):
72+
""" Create a path representation for this item. """
73+
if self.parent:
74+
return self.parent.path + [self.key if self.index is None else self.index]
75+
else:
76+
return ['$']
77+
78+
@property
79+
def ld_value(self):
80+
""" Retrun a representation that is suitable as a value in expanded JSON-LD. """
81+
return self._data
82+
83+
def _to_python(self, full_iri, ld_value):
84+
if full_iri == "@id":
85+
value = ld_value
86+
elif full_iri == "@type":
87+
value = [
88+
self.ld_proc.compact_iri(self.active_ctx, ld_type)
89+
for ld_type in ld_value
90+
]
91+
if len(value) == 1:
92+
value = value[0]
93+
else:
94+
value, ld_output = self.ld_proc.apply_typemap(ld_value, "python", "ld_container",
95+
parent=self, key=full_iri)
96+
if ld_output is None:
97+
raise TypeError(full_iri, ld_value)
98+
99+
return value
100+
101+
def _to_expanded_json(self, key, value):
102+
if key == "@id":
103+
ld_value = self.ld_proc.expand_iri(self.active_ctx, value)
104+
elif key == "@type":
105+
if not isinstance(value, list):
106+
value = [value]
107+
ld_value = [self.ld_proc.expand_iri(self.active_ctx, ld_type) for ld_type in value]
108+
else:
109+
short_key = self.ld_proc.compact_iri(self.active_ctx, key)
110+
if ':' in short_key:
111+
prefix, short_key = short_key.split(':', 1)
112+
ctx_value = self.ld_proc.get_context_value(self.active_ctx, prefix, "@id")
113+
active_ctx = self.ld_proc.process_context(self.active_ctx, [ctx_value],
114+
{"documentLoader": bundled_loader})
115+
else:
116+
active_ctx = self.active_ctx
117+
ld_type = self.ld_proc.get_context_value(active_ctx, short_key, "@type")
118+
if ld_type == "@id":
119+
ld_value = [{"@id": value}]
120+
ld_output = "expanded_json"
121+
else:
122+
ld_value, ld_output = self.ld_proc.apply_typemap(value, "expanded_json", "json",
123+
parent=self, key=key)
124+
if ld_output == "json":
125+
ld_value = self.ld_proc.expand(ld_value, {"expandContext": self.full_context,
126+
"documentLoader": bundled_loader})
127+
elif ld_output != "expanded_json":
128+
raise TypeError(f"Cannot convert {type(value)}")
129+
130+
return ld_value
131+
132+
def __repr__(self):
133+
return f'{type(self).__name__}({self._data[0]})'
134+
135+
def __str__(self):
136+
return str(self.to_python())
137+
138+
def compact(self, context=None):
139+
return self.ld_proc.compact(
140+
self.ld_value,
141+
context or self.context,
142+
{"documentLoader": bundled_loader, "skipExpand": True}
143+
)
144+
145+
def to_python(self):
146+
raise NotImplementedError()
147+
148+
@classmethod
149+
def merge_to_list(cls, *args):
150+
if not args:
151+
return []
152+
153+
head, *tail = args
154+
if isinstance(head, list):
155+
return [*head, *cls.merge_to_list(*tail)]
156+
else:
157+
return [head, *cls.merge_to_list(*tail)]
158+
159+
@classmethod
160+
def is_ld_node(cls, ld_value):
161+
return isinstance(ld_value, list) and len(ld_value) == 1 and isinstance(ld_value[0], dict)
162+
163+
@classmethod
164+
def is_ld_id(cls, ld_value):
165+
return cls.is_ld_node(ld_value) and cls.is_json_id(ld_value[0])
166+
167+
@classmethod
168+
def is_ld_value(cls, ld_value):
169+
return cls.is_ld_node(ld_value) and "@value" in ld_value[0]
170+
171+
@classmethod
172+
def is_typed_ld_value(cls, ld_value):
173+
return cls.is_ld_value(ld_value) and "@type" in ld_value[0]
174+
175+
@classmethod
176+
def is_json_id(cls, ld_value):
177+
return isinstance(ld_value, dict) and ["@id"] == [*ld_value.keys()]
178+
179+
@classmethod
180+
def is_json_value(cls, ld_value):
181+
return isinstance(ld_value, dict) and "@value" in ld_value
182+
183+
@classmethod
184+
def is_typed_json_value(cls, ld_value):
185+
return cls.is_json_value(ld_value) and "@type" in ld_value
186+
187+
@classmethod
188+
def typed_ld_to_py(cls, data, **kwargs):
189+
ld_value = data[0]['@value']
190+
191+
return ld_value
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SPDX-FileCopyrightText: 2025 German Aerospace Center (DLR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Meinel
6+
7+
8+
CODEMETA_PREFIX = "https://doi.org/10.5063/schema/codemeta-2.0"
9+
CODEMETA_CONTEXT = [CODEMETA_PREFIX]
10+
11+
SCHEMA_ORG_PREFIX = "http://schema.org/"
12+
SCHEMA_ORG_CONTEXT = [{"schema": SCHEMA_ORG_PREFIX}]
13+
14+
PROV_PREFIX = "http://www.w3.org/ns/prov#"
15+
PROV_CONTEXT = [{"prov": PROV_PREFIX}]
16+
17+
HERMES_RT_PREFIX = 'https://schema.software-metadata.pub/hermes-runtime/1.0/'
18+
HERMES_RT_CONTEXT = [{'hermes-rt': HERMES_RT_PREFIX}]
19+
HERMES_CONTENT_CONTEXT = [{'hermes': 'https://schema.software-metadata.pub/hermes-content/1.0/'}]
20+
21+
HERMES_CONTEXT = [{**HERMES_RT_CONTEXT[0], **HERMES_CONTENT_CONTEXT[0]}]
22+
23+
HERMES_BASE_CONTEXT = [*CODEMETA_CONTEXT, {**SCHEMA_ORG_CONTEXT[0], **HERMES_CONTENT_CONTEXT[0]}]
24+
HERMES_PROV_CONTEXT = [{**SCHEMA_ORG_CONTEXT[0], **HERMES_RT_CONTEXT[0], **PROV_CONTEXT[0]}]
25+
26+
ALL_CONTEXTS = [*CODEMETA_CONTEXT, {**SCHEMA_ORG_CONTEXT[0], **PROV_CONTEXT[0], **HERMES_CONTEXT[0]}]
27+
28+
29+
class ContextPrefix:
30+
def __init__(self, context):
31+
self.context = context
32+
self.prefix = {}
33+
34+
for ctx in self.context:
35+
if isinstance(ctx, str):
36+
ctx = {None: ctx}
37+
38+
self.prefix.update({
39+
prefix: base_url
40+
for prefix, base_url in ctx.items()
41+
if isinstance(base_url, str)
42+
})
43+
44+
def __getitem__(self, item):
45+
if not isinstance(item, str):
46+
prefix, name = item
47+
elif ':' in item:
48+
prefix, name = item.split(':', 1)
49+
if name.startswith('://'):
50+
prefix, name = True, item
51+
else:
52+
prefix, name = None, item
53+
54+
if prefix in self.prefix:
55+
item = self.prefix[prefix] + name
56+
57+
return item
58+
59+
60+
iri_map = ContextPrefix(ALL_CONTEXTS)

0 commit comments

Comments
 (0)