Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4c1631d
Merge branch 'refactor/data-model' into refactor/384-test-ld_dict
sdruskat Sep 9, 2025
1feddda
Add basic implementation of API class
sdruskat Sep 9, 2025
74ba45d
Test initialization of API class
sdruskat Sep 9, 2025
79575b8
Test API object initiatlization with and without data
sdruskat Sep 17, 2025
69f6a24
Test API object initialization with nested object
sdruskat Sep 17, 2025
8e1a38b
Test appending objects to model via API
sdruskat Sep 17, 2025
b65989e
Test model building via API object
sdruskat Sep 17, 2025
59180c7
added an add method to SoftwareMetadata and improved __init__ of it a…
Sep 25, 2025
daed5d3
Change existing test to assume returned lists
sdruskat Sep 26, 2025
4583915
Add test for harvesting case
sdruskat Sep 26, 2025
6808272
Add more comprehensive usage test
sdruskat Sep 26, 2025
2f7eadf
Add new license annotation for Python files
sdruskat Sep 26, 2025
0f32494
changed conversions of types to output ld_lists for every item in a dict
Sep 26, 2025
8298e49
added some tests for the conversions and formated to satisfy flake8
Sep 26, 2025
3a8bfbe
added three more conversions for container to expanded json
Sep 26, 2025
2ef89d3
always return a list when getting an item from ld_dict
Sep 26, 2025
2db93cf
added tests and fixed issues
Sep 26, 2025
1721325
clean up
Oct 2, 2025
9be8041
removed tests of unclear matters (@type and @context fields) and adde…
Oct 6, 2025
7adb02f
Merge branch 'refactor/data-model' into refactor/423-implement-public…
SKernchen Dec 19, 2025
6f039e8
slightly adjusted tests and fixed miniature bugs in ld_container and …
Dec 19, 2025
c2b9c4f
cleaned up __init__.py
Jan 5, 2026
bd1a19f
ran 'poetry lock'
Jan 5, 2026
9527e26
updated type hints to be supported by python 3.10
Jan 5, 2026
97d9d95
update type hints and began commenting ld_dict
Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ path = ["REUSE.toml"]
precedence = "aggregate"
SPDX-FileCopyrightText = "German Aerospace Center (DLR), Helmholtz-Zentrum Dresden-Rossendorf, Forschungszentrum Jülich"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = ["src/**/*.py", "test/**/*.py"]
precedence = "aggregate"
SPDX-FileCopyrightText = "German Aerospace Center (DLR), Helmholtz-Zentrum Dresden-Rossendorf, Forschungszentrum Jülich"
SPDX-License-Identifier = "Apache-2.0"
46 changes: 7 additions & 39 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ pytest-cov = "^3.0.0"
taskipy = "^1.10.3"
flake8 = "^5.0.4"
requests-mock = "^1.10.0"
pytest-httpserver = "^1.1.3"

# Packages for developers for creating documentation
[tool.poetry.group.docs]
Expand Down
2 changes: 2 additions & 0 deletions src/hermes/model/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
#
# SPDX-License-Identifier: Apache-2.0

from hermes.model.api import SoftwareMetadata
10 changes: 10 additions & 0 deletions src/hermes/model/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from hermes.model.types import ld_dict

from hermes.model.types.ld_context import ALL_CONTEXTS


class SoftwareMetadata(ld_dict):

def __init__(self, data: dict = None, extra_vocabs: dict[str, str] = None) -> None:
ctx = ALL_CONTEXTS + [{**extra_vocabs}] if extra_vocabs is not None else ALL_CONTEXTS
super().__init__([ld_dict.from_dict(data, context=ctx).data_dict if data else {}], context=ctx)
47 changes: 4 additions & 43 deletions src/hermes/model/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,19 @@
# SPDX-FileContributor: Michael Meinel
# SPDX-FileContributor: Michael Fritzsche

from datetime import date, time, datetime

from .ld_container import ld_container
from .ld_list import ld_list
from .ld_dict import ld_dict
from .ld_context import iri_map
from .ld_list import ld_list
from .pyld_util import JsonLdProcessor


_TYPEMAP = [
# Conversion routines for ld_container
(
lambda c: isinstance(c, ld_container),
{
"ld_container": lambda c, **_: c,
"json": lambda c, **_: c.compact(),
"expanded_json": lambda c, **_: c.ld_value,
}
),
# Conversion routine for ld_container
(lambda c: isinstance(c, ld_container), {"ld_container": lambda c, **_: c}),

# Wrap item from ld_dict in ld_list
(ld_list.is_ld_list, {"ld_container": ld_list}),
(lambda c: isinstance(c, list), {"ld_container": lambda c, **kw: ld_list(c, **kw)}),
(lambda c: isinstance(c, list), {"ld_container": ld_list}),

# pythonize items from lists (expanded set is already handled above)
(ld_container.is_json_id, {"python": lambda c, **_: c["@id"]}),
Expand All @@ -36,35 +26,6 @@
(ld_list.is_container, {"ld_container": lambda c, **kw: ld_list([c], **kw)}),
(ld_dict.is_json_dict, {"ld_container": lambda c, **kw: ld_dict([c], **kw)}),
(lambda v: isinstance(v, str), {"python": lambda v, parent, **_: parent.ld_proc.compact_iri(parent.active_ctx, v)}),

# Convert internal data types to expanded_json
(ld_container.is_json_id, {"expanded_json": lambda c, **_: [c]}),
(ld_container.is_ld_id, {"expanded_json": lambda c, **_: c}),
(ld_container.is_json_value, {"expanded_json": lambda c, **_: [c]}),
(ld_container.is_ld_value, {"expanded_json": lambda c, **_: c}),
(ld_dict.is_json_dict, {"expanded_json": lambda c, **kw: ld_dict.from_dict(c, **kw).ld_value}),
(
ld_list.is_container,
{"expanded_json": lambda c, **kw: ld_list.from_list(ld_list.get_item_list_from_container(c), **kw).ld_value}
),
(
ld_list.is_ld_list,
{"expanded_json": lambda c, **kw: ld_list.from_list(ld_list.get_item_list_from_container(c[0]), **kw).ld_value}
),
(lambda c: isinstance(c, list), {"expanded_json": lambda c, **kw: ld_list.from_list(c, **kw).ld_value}),
(lambda v: isinstance(v, (int, float, str, bool)), {"expanded_json": lambda v, **_: [{"@value": v}]}),
(
lambda v: isinstance(v, datetime),
{"expanded_json": lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:DateTime"]}]}
),
(
lambda v: isinstance(v, date),
{"expanded_json": lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:Date"]}]}
),
(
lambda v: isinstance(v, time),
{"expanded_json": lambda v, **_: [{"@value": v.isoformat(), "@type": iri_map["schema:Time"]}]}
),
]


Expand Down
15 changes: 10 additions & 5 deletions src/hermes/model/types/ld_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
from .pyld_util import JsonLdProcessor, bundled_loader

from datetime import date, time, datetime
from typing import Union, Self, Any
from typing import Union, Any
from typing_extensions import Self


JSON_LD_CONTEXT_DICT = dict[str, Union[str, "JSON_LD_CONTEXT_DICT"]]
BASIC_TYPE = Union[str, float, int, bool]
EXPANDED_JSON_LD_VALUE = list[dict[str, Union["EXPANDED_JSON_LD_VALUE", BASIC_TYPE]]]
EXPANDED_JSON_LD_VALUE = list[Union[
dict[str, Union["EXPANDED_JSON_LD_VALUE", BASIC_TYPE]],
"EXPANDED_JSON_LD_VALUE",
str
]]
COMPACTED_JSON_LD_VALUE = Union[
list[Union[dict[str, Union["COMPACTED_JSON_LD_VALUE", BASIC_TYPE]], BASIC_TYPE]],
dict[str, Union["COMPACTED_JSON_LD_VALUE", BASIC_TYPE]],
Expand Down Expand Up @@ -64,7 +69,7 @@ def __init__(
self: Self,
data: EXPANDED_JSON_LD_VALUE,
*,
parent: Union["ld_container", None] = None,
parent: Union[Self, None] = None,
key: Union[str, None] = None,
index: Union[int, None] = None,
context: Union[list[Union[str, JSON_LD_CONTEXT_DICT]], None] = None,
Expand Down Expand Up @@ -237,7 +242,7 @@ def _to_expanded_json(
# while searching build a path such that it leads from the found ld_dicts ld_value to selfs data_dict/ item_list
parent = self
path = []
while parent.__class__.__name__ != "ld_dict":
while parent.__class__.__name__ not in {"ld_dict", "SoftwareMetadata"}:
if parent.container_type == "@list":
path.extend(["@list", 0])
elif parent.container_type == "@graph":
Expand All @@ -250,7 +255,7 @@ def _to_expanded_json(
# if neither self nor any of its parents is a ld_dict:
# create a dict with the key of the outer most parent of self and this parents ld_value as a value
# this dict is stored in an ld_container and simulates the most minimal JSON-LD object possible
if parent.__class__.__name__ != "ld_dict":
if parent.__class__.__name__ not in {"ld_dict", "SoftwareMetadata"}:
key = self.ld_proc.expand_iri(parent.active_ctx, parent.key)
parent = ld_container([{key: parent._data}])
path.append(0)
Expand Down
80 changes: 56 additions & 24 deletions src/hermes/model/types/ld_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,73 @@
# SPDX-FileContributor: Michael Meinel
# SPDX-FileContributor: Michael Fritzsche

from .ld_container import ld_container

from .pyld_util import bundled_loader
from .ld_container import (
ld_container,
JSON_LD_CONTEXT_DICT,
EXPANDED_JSON_LD_VALUE,
PYTHONIZED_LD_CONTAINER,
JSON_LD_VALUE,
TIME_TYPE,
BASIC_TYPE,
)

from collections.abc import KeysView
from types import NotImplementedType
from typing import Union, Any
from typing_extensions import Self


class ld_dict(ld_container):
"""
An JSON-LD container resembling a dict.
See also :class:`ld_container`

:cvar container_type: A type used as a placeholder to represent "no default".
:cvartype container_type: type[str]
"""
_NO_DEFAULT = type("NO DEFAULT")

def __init__(self, data, *, parent=None, key=None, index=None, context=None):
def __init__(
self: Self,
data: list[dict[str, EXPANDED_JSON_LD_VALUE]],
*,
parent: Union[ld_container, None] = None,
key: Union[str, None] = None,
index: Union[int, None] = None,
context: Union[list[Union[str, JSON_LD_CONTEXT_DICT]], None] = None
) -> None:
if not self.is_ld_dict(data):
raise ValueError("The given data does not represent a ld_dict.")
super().__init__(data, parent=parent, key=key, index=index, context=context)

self.data_dict = data[0]

def __getitem__(self, key):
def __getitem__(self: Self, key: str) -> list[Union[BASIC_TYPE, TIME_TYPE, ld_container]]:
full_iri = self.ld_proc.expand_iri(self.active_ctx, key)
if full_iri not in self.data_dict:
self[full_iri] = []
ld_value = self.data_dict[full_iri]
return self._to_python(full_iri, ld_value)

def __setitem__(self, key, value):
def __setitem__(self: Self, key: str, value: Union[JSON_LD_VALUE, BASIC_TYPE, TIME_TYPE, ld_container]) -> None:
full_iri = self.ld_proc.expand_iri(self.active_ctx, key)
if value is None:
del self[full_iri]
return
ld_value = self._to_expanded_json({full_iri: value})
self.data_dict.update(ld_value)

def __delitem__(self, key):
def __delitem__(self: Self, key: str) -> None:
full_iri = self.ld_proc.expand_iri(self.active_ctx, key)
del self.data_dict[full_iri]

def __contains__(self, key):
def __contains__(self: Self, key: str) -> bool:
full_iri = self.ld_proc.expand_iri(self.active_ctx, key)
# FIXME: is that good?
return full_iri in self.data_dict

def __eq__(self, other):
def __eq__(self: Self, other: Any) -> Union[bool, NotImplementedType]: # FIXME: give another type hint to other?
if not isinstance(other, (dict, ld_dict)):
return NotImplemented
if ld_container.is_json_id(other):
Expand All @@ -59,35 +92,34 @@ def __eq__(self, other):
if unique_keys and unique_keys != {"@id"}:
return False
for key in keys_self.intersection(keys_other):
item = self[key]
other_item = other[key]
res = item.__eq__(other_item)
if res == NotImplemented:
res = other_item.__eq__(item)
if res is False or res == NotImplemented: # res is not True
if self[key] != other[key]:
return False
return True

def __ne__(self, other):
def __ne__(self: Self, other: Any) -> Union[bool, NotImplementedType]: # FIXME: give another type hint to other?
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x

def get(self, key, default=_NO_DEFAULT):
def get(
self: Self, key: str, default: Any = _NO_DEFAULT
) -> Union[list[Union[BASIC_TYPE, TIME_TYPE, ld_container]], Any]:
try:
value = self[key]
return value
return self[key]
except KeyError as e:
if default is not ld_dict._NO_DEFAULT:
return default
raise e

def update(self, other):
if default is self._NO_DEFAULT:
raise e
return default

def update(
self: Self,
other: Union[dict[str, Union[JSON_LD_VALUE, BASIC_TYPE, TIME_TYPE, ld_container]], "ld_dict"]
) -> None:
for key, value in other.items():
self[key] = value

def keys(self):
def keys(self: Self) -> KeysView[str]:
return self.data_dict.keys()

def compact_keys(self):
Expand Down
Loading
Loading