From d5234bfb13fb89c9a43250bfbe4f855b9a097b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Sun, 9 Nov 2025 20:59:38 +0100 Subject: [PATCH 01/48] implement access role models --- dsms/knowledge/groups.py | 19 ++ dsms/knowledge/properties/__init__.py | 2 + dsms/knowledge/properties/access.py | 149 +++++++++++++ dsms/knowledge/utils.py | 16 ++ tests/test_access.py | 297 ++++++++++++++++++++++++++ 5 files changed, 483 insertions(+) create mode 100644 dsms/knowledge/groups.py create mode 100644 dsms/knowledge/properties/access.py create mode 100644 tests/test_access.py diff --git a/dsms/knowledge/groups.py b/dsms/knowledge/groups.py new file mode 100644 index 0000000..eee0e2d --- /dev/null +++ b/dsms/knowledge/groups.py @@ -0,0 +1,19 @@ +"""DSMS User Groups Module.""" + +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel, Field + + +class Group(BaseModel): + """User Group Model""" + + id: UUID = Field(..., description="The unique identifier of the group.") + name: str = Field(..., description="The name of the group.") + subgroups: Optional[List["Group"]] = Field( + None, description="A list of subgroups." + ) + + +Group.model_rebuild() diff --git a/dsms/knowledge/properties/__init__.py b/dsms/knowledge/properties/__init__.py index 1c0c3e8..6670969 100644 --- a/dsms/knowledge/properties/__init__.py +++ b/dsms/knowledge/properties/__init__.py @@ -7,6 +7,7 @@ Annotation, AnnotationList, ) +from dsms.knowledge.properties.access import KItemAccessProperties from dsms.knowledge.properties.apps import App, AppList from dsms.knowledge.properties.authors import Author from dsms.knowledge.properties.contacts import ContactInfo @@ -49,4 +50,5 @@ "DataFrameContainer", "Column", "KItemRelationshipModel", + "KItemAccessProperties", ] diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py new file mode 100644 index 0000000..de4dd62 --- /dev/null +++ b/dsms/knowledge/properties/access.py @@ -0,0 +1,149 @@ +"""KItem Access Property Module""" + +from enum import Enum +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field, field_validator + + +class OperationType(str, Enum): + """Operation Types Enum""" + + CREATE = "create" + READ = "read" + UPDATE = "update" + DELETE = "delete" + MANAGE = "manage" + + +class Role(str, Enum): + """Role Enum""" + + OWNER = "owner" + USER = "user" + CONTRIBUTOR = "contributor" + ADMIN = "admin" + + +class RoleMapping(List[OperationType], Enum): + """Role Mapping Enum""" + + OWNER = [ + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ] + USER = [OperationType.READ] + CONTRIBUTOR = [OperationType.READ, OperationType.UPDATE] + ADMIN = [ + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ] + + @classmethod + def get_operations(cls, role: Role) -> List[OperationType]: + """Get operations for a role""" + return getattr(cls, role.value.upper()) + + +class BaseAccessProperty(BaseModel): + """KItem Access Property Model""" + + role: Role = Field( + ..., + description="Defines the role mapping for access control.", + example=RoleMapping.OWNER, + ) + + @property + def access_level(self) -> List[OperationType]: + """Set access level based on role""" + return RoleMapping.get_operations(self.role) + + +class UserAccessProperty(BaseAccessProperty): + """KItem User Access Property Model""" + + user_id: str = Field( + ..., + description="The unique identifier of the user.", + example="1a3b5c7d-9e0f-4g2h-8i1j-2k3l4m5n6o7p", + ) + + +class GroupAccessProperty(BaseAccessProperty): + """KItem Group Access Property Model""" + + group_id: str = Field( + ..., + description="The unique identifier of the group.", + example="g1h2i3j4-k5l6-m7n8-o9p0-q1r2s3t4u5v6", + ) + + +class KItemAccessProperties(BaseModel): + """KItem Access Properties Model""" + + user_access: Optional[List[UserAccessProperty]] = Field( + [], + description="List of user access properties.", + ) + group_access: Optional[List[GroupAccessProperty]] = Field( + [], + description="List of group access properties.", + ) + + @field_validator("user_access", "group_access", mode="before") + @classmethod + def check_duplicates(cls, v): + """Ensure no duplicate user or group IDs""" + if v is None: + return [] + seen = set() + for item in v: + identifier = ( + item.user_id + if isinstance(item, UserAccessProperty) + else item.group_id + ) + if identifier in seen: + raise ValueError(f"Duplicate identifier found: {identifier}") + seen.add(identifier) + return v + + @property + def by_user(self) -> Dict[str, UserAccessProperty]: + """Get user access properties""" + return {uap.user_id: uap for uap in self.user_access} + + @property + def by_group(self) -> Dict[str, GroupAccessProperty]: + """Get group access properties""" + return {gap.group_id: gap for gap in self.group_access} + + @property + def operation_by_user(self) -> Dict[OperationType, List[str]]: + """Get access properties by operation type""" + operation_dict: Dict[OperationType, List[str]] = {} + for uap in self.user_access: + for operation in uap.access_level: + if operation not in operation_dict: + operation_dict[operation] = [] + if uap.user_id not in operation_dict[operation]: + operation_dict[operation].append(uap.user_id) + return operation_dict + + @property + def operation_by_group(self) -> Dict[OperationType, List[str]]: + """Get group access properties by operation type""" + operation_dict: Dict[OperationType, List[str]] = {} + for gap in self.group_access: + for operation in gap.access_level: + if operation not in operation_dict: + operation_dict[operation] = [] + if gap.group_id not in operation_dict[operation]: + operation_dict[operation].append(gap.group_id) + return operation_dict diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 40de85c..65b6378 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -24,6 +24,8 @@ from dsms.knowledge.search import SearchResult, KItemListModel # isort:skip^ +from dsms.knowledge.groups import Group # isort:skip + from dsms.core.session import Session # isort:skip if TYPE_CHECKING: @@ -1462,3 +1464,17 @@ def generate_mapping(ktype_id: str, webform: dict): else: mapping = mappings return mapping + + +def _get_user_groups(dsms: "DSMS"): + """Fetch all user groups from the DSMS backend.""" + + response = _perform_request( + dsms, + "api/users/groups", + "get", + ) + if not response.ok: + raise ConnectionError(f"Failed to fetch user groups: {response.text}") + groups = response.json() + return [Group(**group) for group in groups] diff --git a/tests/test_access.py b/tests/test_access.py new file mode 100644 index 0000000..0596191 --- /dev/null +++ b/tests/test_access.py @@ -0,0 +1,297 @@ +""""Tests for Access Property Module""" + +from typing import List + +import pytest +from pydantic import ValidationError + +from dsms.knowledge.properties.access import ( + BaseAccessProperty, + GroupAccessProperty, + KItemAccessProperties, + OperationType, + Role, + UserAccessProperty, +) + + +@pytest.fixture +def sample_user_access() -> List[UserAccessProperty]: + """Create sample user access properties""" + return [ + UserAccessProperty(user_id="user1", role=Role.OWNER), + UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), + ] + + +@pytest.fixture +def sample_group_access() -> List[GroupAccessProperty]: + """Create sample group access properties""" + return [ + GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group2", role=Role.USER), + ] + + +@pytest.fixture +def access_properties( + sample_user_access, sample_group_access +) -> KItemAccessProperties: + """Create KItemAccessProperties instance with sample data""" + return KItemAccessProperties( + user_access=sample_user_access, + group_access=sample_group_access, + ) + + +def test_access_level_owner(): + """Test access_level property for OWNER role""" + prop = BaseAccessProperty(role=Role.OWNER) + expected = [ + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ] + assert prop.access_level == expected + + +def test_access_level_user(): + """Test access_level property for USER role""" + prop = BaseAccessProperty(role=Role.USER) + expected = [OperationType.READ] + assert prop.access_level == expected + + +def test_access_level_contributor(): + """Test access_level property for CONTRIBUTOR role""" + prop = BaseAccessProperty(role=Role.CONTRIBUTOR) + expected = [OperationType.READ, OperationType.UPDATE] + assert prop.access_level == expected + + +def test_access_level_admin(): + """Test access_level property for ADMIN role""" + prop = BaseAccessProperty(role=Role.ADMIN) + expected = [ + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ] + assert prop.access_level == expected + + +@pytest.mark.usefixtures("access_properties") +def test_by_user_property(access_properties): + """Test by_user property returns correct user mapping""" + result = access_properties.by_user + + assert len(result) == 3 + assert "user1" in result + assert "user2" in result + assert "user3" in result + + assert result["user1"].role == Role.OWNER + assert result["user2"].role == Role.USER + assert result["user3"].role == Role.CONTRIBUTOR + + +@pytest.mark.usefixtures("access_properties") +def test_by_group_property(access_properties): + """Test by_group property returns correct group mapping""" + result = access_properties.by_group + + assert len(result) == 2 + assert "group1" in result + assert "group2" in result + + assert result["group1"].role == Role.ADMIN + assert result["group2"].role == Role.USER + + +@pytest.mark.usefixtures("access_properties") +def test_operation_by_user_property(access_properties): + """Test operation_by_user property returns correct operation mapping""" + result = access_properties.operation_by_user + + # user1 (OWNER): READ, UPDATE, DELETE, MANAGE + # user2 (USER): READ + # user3 (CONTRIBUTOR): READ, UPDATE + + expected_read = ["user1", "user2", "user3"] + expected_update = ["user1", "user3"] + expected_delete = ["user1"] + expected_manage = ["user1"] + + assert set(result[OperationType.READ]) == set(expected_read) + assert set(result[OperationType.UPDATE]) == set(expected_update) + assert set(result[OperationType.DELETE]) == set(expected_delete) + assert set(result[OperationType.MANAGE]) == set(expected_manage) + + +@pytest.mark.usefixtures("access_properties") +def test_operation_by_group_property(access_properties): + """Test operation_by_group property returns correct operation mapping""" + result = access_properties.operation_by_group + + # group1 (ADMIN): READ, UPDATE, DELETE, MANAGE + # group2 (USER): READ + + expected_read = ["group1", "group2"] + expected_update = ["group1"] + expected_delete = ["group1"] + expected_manage = ["group1"] + + assert set(result[OperationType.READ]) == set(expected_read) + assert set(result[OperationType.UPDATE]) == set(expected_update) + assert set(result[OperationType.DELETE]) == set(expected_delete) + assert set(result[OperationType.MANAGE]) == set(expected_manage) + + +def test_operation_by_user_multiple_same_operation(): + """Test operation_by_user with multiple users having same operations""" + user_access = [ + UserAccessProperty(user_id="user1", role=Role.USER), + UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), + ] + props = KItemAccessProperties(user_access=user_access) + result = props.operation_by_user + + # All users should have READ access + assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} + # Only user3 (CONTRIBUTOR) should have UPDATE access + assert result[OperationType.UPDATE] == ["user3"] + + +def test_operation_by_group_multiple_same_operation(): + """Test operation_by_group with multiple groups having same operations""" + group_access = [ + GroupAccessProperty(group_id="group1", role=Role.USER), + GroupAccessProperty(group_id="group2", role=Role.USER), + GroupAccessProperty(group_id="group3", role=Role.ADMIN), + ] + props = KItemAccessProperties(group_access=group_access) + result = props.operation_by_group + + # All groups should have READ access + assert set(result[OperationType.READ]) == {"group1", "group2", "group3"} + # Only group3 (ADMIN) should have MANAGE access + assert result[OperationType.MANAGE] == ["group3"] + + +def test_model_creation_with_defaults(): + """Test model creation with default values""" + props = KItemAccessProperties() + + assert props.user_access == [] + assert props.group_access == [] + assert props.by_user == {} + assert props.by_group == {} + assert props.operation_by_user == {} + assert props.operation_by_group == {} + + +def test_user_access_property_creation(): + """Test UserAccessProperty creation and access_level inheritance""" + user_prop = UserAccessProperty(user_id="test_user", role=Role.CONTRIBUTOR) + + assert user_prop.user_id == "test_user" + assert user_prop.role == Role.CONTRIBUTOR + assert user_prop.access_level == [OperationType.READ, OperationType.UPDATE] + + +def test_duplicate_user_ids_raises_error(): + """Test that duplicate user IDs raise ValueError""" + user_access = [ + UserAccessProperty(user_id="user1", role=Role.OWNER), + UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty( + user_id="user1", role=Role.CONTRIBUTOR + ), # Duplicate + ] + + with pytest.raises(ValidationError) as exc_info: + KItemAccessProperties(user_access=user_access, group_access=[]) + + # Check that the ValueError with the correct message is included + error_details = exc_info.value.errors() + assert len(error_details) == 1 + assert error_details[0]["type"] == "value_error" + assert "Duplicate identifier found: user1" in str( + error_details[0]["ctx"]["error"] + ) + + +def test_duplicate_group_ids_raises_error(): + """Test that duplicate group IDs raise ValueError""" + group_access = [ + GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group2", role=Role.USER), + GroupAccessProperty( + group_id="group1", role=Role.CONTRIBUTOR + ), # Duplicate + ] + + with pytest.raises(ValidationError) as exc_info: + KItemAccessProperties(user_access=[], group_access=group_access) + + error_details = exc_info.value.errors() + assert len(error_details) == 1 + assert error_details[0]["type"] == "value_error" + assert "Duplicate identifier found: group1" in str( + error_details[0]["ctx"]["error"] + ) + + +def test_both_user_and_group_duplicates_raises_multiple_errors(): + """Test that duplicates in both user and group access raise multiple errors""" + user_access = [ + UserAccessProperty(user_id="user1", role=Role.OWNER), + UserAccessProperty(user_id="user1", role=Role.USER), # Duplicate + ] + group_access = [ + GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group1", role=Role.USER), # Duplicate + ] + + with pytest.raises(ValidationError) as exc_info: + KItemAccessProperties( + user_access=user_access, group_access=group_access + ) + + error_details = exc_info.value.errors() + assert len(error_details) == 2 + + # Check both errors + user_error = next( + err for err in error_details if err["loc"] == ("user_access",) + ) + group_error = next( + err for err in error_details if err["loc"] == ("group_access",) + ) + + assert "Duplicate identifier found: user1" in str( + user_error["ctx"]["error"] + ) + assert "Duplicate identifier found: group1" in str( + group_error["ctx"]["error"] + ) + + +def test_case_sensitive_ids(): + """Test that IDs are case sensitive (no duplicates if different case)""" + user_access = [ + UserAccessProperty(user_id="User1", role=Role.OWNER), + UserAccessProperty(user_id="user1", role=Role.USER), # Different case + UserAccessProperty( + user_id="USER1", role=Role.CONTRIBUTOR + ), # Different case + ] + + # Should not throw an exception + props = KItemAccessProperties(user_access=user_access, group_access=[]) + + assert len(props.user_access) == 3 From 6f5ccfcdebf245a18716f285e9dbc72e6d70a4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 27 Nov 2025 15:08:54 +0100 Subject: [PATCH 02/48] update access level --- dsms/knowledge/properties/access.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index de4dd62..a1e8c02 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -1,6 +1,6 @@ """KItem Access Property Module""" -from enum import Enum +from enum import Enum, auto from typing import Dict, List, Optional from pydantic import BaseModel, Field, field_validator @@ -16,13 +16,13 @@ class OperationType(str, Enum): MANAGE = "manage" -class Role(str, Enum): +class Role(int, Enum): """Role Enum""" - OWNER = "owner" - USER = "user" - CONTRIBUTOR = "contributor" - ADMIN = "admin" + USER = auto() + CONTRIBUTOR = auto() + OWNER = auto() + ADMIN = auto() class RoleMapping(List[OperationType], Enum): @@ -46,7 +46,7 @@ class RoleMapping(List[OperationType], Enum): @classmethod def get_operations(cls, role: Role) -> List[OperationType]: """Get operations for a role""" - return getattr(cls, role.value.upper()) + return getattr(cls, role.name.upper()) class BaseAccessProperty(BaseModel): From 96011b08522658f71efa2dde1d36754ea0173c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 27 Nov 2025 15:24:55 +0100 Subject: [PATCH 03/48] update tests --- tests/test_access.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_access.py b/tests/test_access.py index 0596191..655cf2c 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -55,6 +55,7 @@ def test_access_level_owner(): OperationType.MANAGE, ] assert prop.access_level == expected + assert prop.role.value == Role.OWNER.value def test_access_level_user(): @@ -62,6 +63,7 @@ def test_access_level_user(): prop = BaseAccessProperty(role=Role.USER) expected = [OperationType.READ] assert prop.access_level == expected + assert prop.role.value == Role.USER.value def test_access_level_contributor(): @@ -69,6 +71,7 @@ def test_access_level_contributor(): prop = BaseAccessProperty(role=Role.CONTRIBUTOR) expected = [OperationType.READ, OperationType.UPDATE] assert prop.access_level == expected + assert prop.role.value == Role.CONTRIBUTOR.value def test_access_level_admin(): @@ -81,6 +84,7 @@ def test_access_level_admin(): OperationType.MANAGE, ] assert prop.access_level == expected + assert prop.role.value == Role.ADMIN.value @pytest.mark.usefixtures("access_properties") @@ -96,6 +100,9 @@ def test_by_user_property(access_properties): assert result["user1"].role == Role.OWNER assert result["user2"].role == Role.USER assert result["user3"].role == Role.CONTRIBUTOR + assert result["user1"].role.value == Role.OWNER.value + assert result["user2"].role.value == Role.USER.value + assert result["user3"].role.value == Role.CONTRIBUTOR.value @pytest.mark.usefixtures("access_properties") @@ -109,6 +116,8 @@ def test_by_group_property(access_properties): assert result["group1"].role == Role.ADMIN assert result["group2"].role == Role.USER + assert result["group1"].role.value == Role.ADMIN.value + assert result["group2"].role.value == Role.USER.value @pytest.mark.usefixtures("access_properties") @@ -166,6 +175,24 @@ def test_operation_by_user_multiple_same_operation(): assert result[OperationType.UPDATE] == ["user3"] +def test_operation_by_group_from_int(): + """Test operation_by_user with multiple users having same operations""" + user_access = [ + UserAccessProperty(user_id="user1", role=1), + UserAccessProperty(user_id="user2", role=1), + UserAccessProperty(user_id="user3", role=2), + ] + props = KItemAccessProperties(user_access=user_access) + result = props.operation_by_user + + # All users should have READ access + assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} + # Only user3 (CONTRIBUTOR) should have UPDATE access + assert result[OperationType.UPDATE] == ["user3"] + assert props.by_user["user1"].role == Role.USER + assert props.by_user["user1"].role.value == Role.USER.value + + def test_operation_by_group_multiple_same_operation(): """Test operation_by_group with multiple groups having same operations""" group_access = [ @@ -295,3 +322,6 @@ def test_case_sensitive_ids(): props = KItemAccessProperties(user_access=user_access, group_access=[]) assert len(props.user_access) == 3 + assert props.by_user["User1"].role == Role.OWNER + assert props.by_user["user1"].role == Role.USER + assert props.by_user["USER1"].role == Role.CONTRIBUTOR From ea6760dd6cc952258db4e8b8796125780f2da655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 27 Nov 2025 16:23:49 +0100 Subject: [PATCH 04/48] add user and group by role method --- dsms/knowledge/properties/access.py | 20 ++++++++++++++++++++ tests/test_access.py | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index a1e8c02..7399a84 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -147,3 +147,23 @@ def operation_by_group(self) -> Dict[OperationType, List[str]]: if gap.group_id not in operation_dict[operation]: operation_dict[operation].append(gap.group_id) return operation_dict + + @property + def user_by_role(self) -> Dict[Role, List[str]]: + """Get users by role""" + role_dict: Dict[Role, List[str]] = {} + for uap in self.user_access: + if uap.role not in role_dict: + role_dict[uap.role] = [] + role_dict[uap.role].append(uap.user_id) + return role_dict + + @property + def group_by_role(self) -> Dict[Role, List[str]]: + """Get groups by role""" + role_dict: Dict[Role, List[str]] = {} + for gap in self.group_access: + if gap.role not in role_dict: + role_dict[gap.role] = [] + role_dict[gap.role].append(gap.group_id) + return role_dict diff --git a/tests/test_access.py b/tests/test_access.py index 655cf2c..b5775cc 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -207,6 +207,8 @@ def test_operation_by_group_multiple_same_operation(): assert set(result[OperationType.READ]) == {"group1", "group2", "group3"} # Only group3 (ADMIN) should have MANAGE access assert result[OperationType.MANAGE] == ["group3"] + assert props.group_by_role[Role.ADMIN] == ["group3"] + assert props.group_by_role[Role.USER] == ["group1", "group2"] def test_model_creation_with_defaults(): @@ -325,3 +327,6 @@ def test_case_sensitive_ids(): assert props.by_user["User1"].role == Role.OWNER assert props.by_user["user1"].role == Role.USER assert props.by_user["USER1"].role == Role.CONTRIBUTOR + assert props.user_by_role[Role.OWNER] == ["User1"] + assert props.user_by_role[Role.USER] == ["user1"] + assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] From 4d5f426611d46ed9185675b860a519ae04b38962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 27 Nov 2025 16:57:00 +0100 Subject: [PATCH 05/48] add user lists --- dsms/core/dsms.py | 13 +++++++++++++ dsms/knowledge/groups.py | 7 +++++++ dsms/knowledge/utils.py | 16 +++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index bbc6a67..62f309d 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -25,12 +25,15 @@ _get_remote_ktypes, _get_process_schemas, _get_webform_schemas, + _get_user_groups, + _get_user_list, ) if TYPE_CHECKING: from typing import Optional from dsms.core.session import Buffers + from dsms.knowledge.groups import Group, User from dsms.knowledge.search import KItemListModel, SearchResult @@ -331,6 +334,16 @@ def session(self) -> "Session": """Return DSMS session""" return self._session + @property + def user_groups(self) -> "List[Group]": + """Return user groups of the DSMS session""" + return _get_user_groups(self) + + @property + def users(self) -> "List[User]": + """Return user list of the DSMS session""" + return _get_user_list(self) + @classmethod def __get_pydantic_core_schema__(cls): """Get validator of the DSMS-object.""" diff --git a/dsms/knowledge/groups.py b/dsms/knowledge/groups.py index eee0e2d..530dd08 100644 --- a/dsms/knowledge/groups.py +++ b/dsms/knowledge/groups.py @@ -6,6 +6,13 @@ from pydantic import BaseModel, Field +class User(BaseModel): + """User Model""" + + id: str = Field(..., description="The unique identifier of the user.") + username: str = Field(..., description="The username of the user.") + + class Group(BaseModel): """User Group Model""" diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 65b6378..8e930ed 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -24,7 +24,7 @@ from dsms.knowledge.search import SearchResult, KItemListModel # isort:skip^ -from dsms.knowledge.groups import Group # isort:skip +from dsms.knowledge.groups import Group, User # isort:skip from dsms.core.session import Session # isort:skip @@ -1478,3 +1478,17 @@ def _get_user_groups(dsms: "DSMS"): raise ConnectionError(f"Failed to fetch user groups: {response.text}") groups = response.json() return [Group(**group) for group in groups] + + +def _get_user_list(dsms: "DSMS"): + """Fetch all users from the DSMS backend.""" + + response = _perform_request( + dsms, + "api/users/", + "get", + ) + if not response.ok: + raise ConnectionError(f"Failed to fetch users: {response.text}") + users = response.json() + return [User(**user) for user in users] From cb3d150e9798638c5ea86815e61961dd7d7eb692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 27 Nov 2025 17:45:17 +0100 Subject: [PATCH 06/48] update retrieval of user groups --- dsms/knowledge/groups.py | 48 ++++++++++++++++++++++++++++++++++++++-- dsms/knowledge/utils.py | 6 ++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/dsms/knowledge/groups.py b/dsms/knowledge/groups.py index 530dd08..f67f416 100644 --- a/dsms/knowledge/groups.py +++ b/dsms/knowledge/groups.py @@ -1,10 +1,12 @@ """DSMS User Groups Module.""" from typing import List, Optional -from uuid import UUID +import yaml from pydantic import BaseModel, Field +from dsms.core.session import Session + class User(BaseModel): """User Model""" @@ -16,11 +18,53 @@ class User(BaseModel): class Group(BaseModel): """User Group Model""" - id: UUID = Field(..., description="The unique identifier of the group.") + id: str = Field(..., description="The unique identifier of the group.") name: str = Field(..., description="The name of the group.") subgroups: Optional[List["Group"]] = Field( None, description="A list of subgroups." ) +class GroupListBase(list): + """Base class for GroupList with utility methods.""" + + def __repr__(self) -> str: + """String representation of the GroupList.""" + return str(self) + + def __str__(self): + """Pretty print the LinkedKItemList""" + from dsms.knowledge.utils import dump_model + + return yaml.dump( + [ + dump_model( + connection, + exclude_extra=Session.dsms.config.hide_properties, + ) + for connection in self + ] + ) + + +class GroupList(list): + """List of Groups with utility methods.""" + + @property + def flat(self) -> List[Group]: + """Return a flat list of all groups and their subgroups.""" + flat_list = [] + + def _flatten(groups: List[Group]): + for group in groups: + flat_list.append( + Group(**group.model_dump(exclude={"subgroups"})) + ) + if group.subgroups: + _flatten(group.subgroups) + + _flatten(self) + return GroupListBase(flat_list) + + Group.model_rebuild() diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 8e930ed..5701dff 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -24,8 +24,6 @@ from dsms.knowledge.search import SearchResult, KItemListModel # isort:skip^ -from dsms.knowledge.groups import Group, User # isort:skip - from dsms.core.session import Session # isort:skip if TYPE_CHECKING: @@ -1468,6 +1466,7 @@ def generate_mapping(ktype_id: str, webform: dict): def _get_user_groups(dsms: "DSMS"): """Fetch all user groups from the DSMS backend.""" + from dsms.knowledge.groups import Group, GroupList response = _perform_request( dsms, @@ -1477,11 +1476,12 @@ def _get_user_groups(dsms: "DSMS"): if not response.ok: raise ConnectionError(f"Failed to fetch user groups: {response.text}") groups = response.json() - return [Group(**group) for group in groups] + return GroupList([Group(**group) for group in groups]) def _get_user_list(dsms: "DSMS"): """Fetch all users from the DSMS backend.""" + from dsms.knowledge.groups import User response = _perform_request( dsms, From e6bbd40398b241c9a77c7f0a7061ec1bbf877aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Fri, 28 Nov 2025 00:41:02 +0100 Subject: [PATCH 07/48] add min access level and related unit tests --- dsms/knowledge/properties/access.py | 22 +++++++++++++++++++++ tests/test_access.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 7399a84..b8f610b 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -48,6 +48,28 @@ def get_operations(cls, role: Role) -> List[OperationType]: """Get operations for a role""" return getattr(cls, role.name.upper()) + @classmethod + def min_access_level(cls, operation: OperationType) -> Role: + """Get minimum role required for an operation""" + return min( + [ + role.value + for role in Role + if operation in cls.get_operations(role) + ] + ) + + @classmethod + def max_access_level(cls, operation: OperationType) -> Role: + """Get maximum role required for an operation""" + return max( + [ + role.value + for role in Role + if operation in cls.get_operations(role) + ] + ) + class BaseAccessProperty(BaseModel): """KItem Access Property Model""" diff --git a/tests/test_access.py b/tests/test_access.py index b5775cc..19a85c1 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -11,6 +11,7 @@ KItemAccessProperties, OperationType, Role, + RoleMapping, UserAccessProperty, ) @@ -58,6 +59,35 @@ def test_access_level_owner(): assert prop.role.value == Role.OWNER.value +def test_minimum_access_level(): + """Test min_access_level method""" + assert RoleMapping.min_access_level(OperationType.READ) == Role.USER.value + assert ( + RoleMapping.min_access_level(OperationType.UPDATE) + == Role.CONTRIBUTOR.value + ) + assert ( + RoleMapping.min_access_level(OperationType.DELETE) == Role.OWNER.value + ) + assert ( + RoleMapping.min_access_level(OperationType.MANAGE) == Role.OWNER.value + ) + + +def test_maximum_access_level(): + """Test max_access_level method""" + assert RoleMapping.max_access_level(OperationType.READ) == Role.ADMIN.value + assert ( + RoleMapping.max_access_level(OperationType.UPDATE) == Role.ADMIN.value + ) + assert ( + RoleMapping.max_access_level(OperationType.DELETE) == Role.ADMIN.value + ) + assert ( + RoleMapping.max_access_level(OperationType.MANAGE) == Role.ADMIN.value + ) + + def test_access_level_user(): """Test access_level property for USER role""" prop = BaseAccessProperty(role=Role.USER) From 3cdf3ff8c339f5e688daa79a707481c06dfe09cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Wed, 3 Dec 2025 17:16:42 +0100 Subject: [PATCH 08/48] update public group types --- dsms/knowledge/groups.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dsms/knowledge/groups.py b/dsms/knowledge/groups.py index f67f416..3e73c5d 100644 --- a/dsms/knowledge/groups.py +++ b/dsms/knowledge/groups.py @@ -1,5 +1,6 @@ """DSMS User Groups Module.""" +from enum import Enum from typing import List, Optional import yaml @@ -8,6 +9,13 @@ from dsms.core.session import Session +class PublicGroupType(str, Enum): + """Enumeration for Public Group Types.""" + + INTERNAL = "dsms:internally-public" + EXTERNAL = "dsms:externally-public" + + class User(BaseModel): """User Model""" From 1f69e8a32ca849d4dbbfecb8a9aa2e9e676d1d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 9 Dec 2025 15:08:55 +0100 Subject: [PATCH 09/48] add access levels and public groups --- dsms/core/configuration.py | 28 ++++++++++++++++- dsms/knowledge/groups/__init__.py | 6 ++++ .../knowledge/{groups.py => groups/models.py} | 12 +++---- dsms/knowledge/groups/public.py | 31 +++++++++++++++++++ dsms/knowledge/properties/access.py | 16 ++++------ 5 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 dsms/knowledge/groups/__init__.py rename dsms/knowledge/{groups.py => groups/models.py} (90%) create mode 100644 dsms/knowledge/groups/public.py diff --git a/dsms/core/configuration.py b/dsms/core/configuration.py index 3b842f6..60f1d72 100644 --- a/dsms/core/configuration.py +++ b/dsms/core/configuration.py @@ -43,7 +43,33 @@ class Loglevel(Enum): WARNING = logging.WARNING -class Configuration(BaseSettings): +class BaseConfiguration(BaseSettings): + """Base Configuration for DSMS-SDK""" + + label_internally_public: str = Field( + "Internally Public", + description="Label to use for KItems marked as `internally_public`.", + ) + + label_externally_public: str = Field( + "Externally Public", + description="Label to use for KItems marked as `externally_public`.", + ) + + id_internally_public: str = Field( + "dsms:internally-public", + description="ID to use for KItems marked as `internally_public`.", + ) + + id_externally_public: str = Field( + "dsms:externally-public", + description="ID to use for KItems marked as `externally_public`.", + ) + + model_config = ConfigDict(use_enum_values=True) + + +class Configuration(BaseConfiguration): """General config for DSMS-SDK""" host_url: AnyUrl = Field( diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py new file mode 100644 index 0000000..8918b66 --- /dev/null +++ b/dsms/knowledge/groups/__init__.py @@ -0,0 +1,6 @@ +"""DSMS User Groups Module.""" + +from .models import Group, GroupList, GroupListBase +from .public import INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP + +__all__ = ["Group", "GroupList", "GroupListBase", "INTERNALLY_PUBLIC_GROUP", "EXTERNALLY_PUBLIC_GROUP"] diff --git a/dsms/knowledge/groups.py b/dsms/knowledge/groups/models.py similarity index 90% rename from dsms/knowledge/groups.py rename to dsms/knowledge/groups/models.py index 3e73c5d..3d8cee6 100644 --- a/dsms/knowledge/groups.py +++ b/dsms/knowledge/groups/models.py @@ -1,6 +1,5 @@ """DSMS User Groups Module.""" -from enum import Enum from typing import List, Optional import yaml @@ -9,13 +8,6 @@ from dsms.core.session import Session -class PublicGroupType(str, Enum): - """Enumeration for Public Group Types.""" - - INTERNAL = "dsms:internally-public" - EXTERNAL = "dsms:externally-public" - - class User(BaseModel): """User Model""" @@ -76,3 +68,7 @@ def _flatten(groups: List[Group]): Group.model_rebuild() +interally_public = Group(id="dsms:internally_public", name="Internally Public") +externally_public = Group( + id="dsms:externally_public", name="Externally Public" +) \ No newline at end of file diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py new file mode 100644 index 0000000..af72c36 --- /dev/null +++ b/dsms/knowledge/groups/public.py @@ -0,0 +1,31 @@ +"""DSMS Public User Groups Module.""" + +from .models import Group + +from dsms.core.session import Session +from dsms.core.configuration import BaseConfiguration + + +if not Session.dsms: + config = BaseConfiguration() +else: + config = Session.dsms.config + +# The internally/externally public group objects will generally +# be served by the user-service, but we define them here for uniquely +# setting the ids and names in a common place. +# They can be adapted through environment variables anyway. +# A common place is needed because the group objects are used in various places +# such as internally within the knowledge service, the user service, and the SDK itself. +# If the IDs and names of these public groups are only delivered in the user service, +# we cannot distinguish them from the ones which come from keycloak - indicating only organizational groups. + +INTERNALLY_PUBLIC_GROUP = Group( + id=config.id_internally_public, + name=config.label_internally_public, +) + +EXTERNALLY_PUBLIC_GROUP = Group( + id=config.id_externally_public, + name=config.label_externally_public, +) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index b8f610b..bcbdcc4 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -52,22 +52,18 @@ def get_operations(cls, role: Role) -> List[OperationType]: def min_access_level(cls, operation: OperationType) -> Role: """Get minimum role required for an operation""" return min( - [ - role.value - for role in Role - if operation in cls.get_operations(role) - ] + role.value + for role in Role + if operation in cls.get_operations(role) ) @classmethod def max_access_level(cls, operation: OperationType) -> Role: """Get maximum role required for an operation""" return max( - [ - role.value - for role in Role - if operation in cls.get_operations(role) - ] + role.value + for role in Role + if operation in cls.get_operations(role) ) From aec63406e14442149637477ed6c255807f7eb4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 9 Dec 2025 16:00:01 +0100 Subject: [PATCH 10/48] update user and group models --- dsms/knowledge/groups/__init__.py | 15 +++++++-- dsms/knowledge/groups/models.py | 55 ++++++++++++++++++++++++++++--- dsms/knowledge/groups/public.py | 12 +++---- dsms/knowledge/utils.py | 4 +-- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index 8918b66..f027c21 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -1,6 +1,15 @@ """DSMS User Groups Module.""" -from .models import Group, GroupList, GroupListBase -from .public import INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP +from .models import BaseGroup, Group, GroupList, GroupListBase, User, UserList +from .public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP -__all__ = ["Group", "GroupList", "GroupListBase", "INTERNALLY_PUBLIC_GROUP", "EXTERNALLY_PUBLIC_GROUP"] +__all__ = [ + "Group", + "GroupList", + "GroupListBase", + "INTERNALLY_PUBLIC_GROUP", + "EXTERNALLY_PUBLIC_GROUP", + "User", + "BaseGroup", + "UserList", +] diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 3d8cee6..4d7698d 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -15,11 +15,48 @@ class User(BaseModel): username: str = Field(..., description="The username of the user.") -class Group(BaseModel): +class UserList(list): + """List of Users with utility methods.""" + + def __repr__(self) -> str: + """String representation of the GroupList.""" + return str(self) + + def __str__(self): + """Pretty print the UserList""" + from dsms.knowledge.utils import dump_model + + return yaml.dump( + [ + dump_model( + connection, + exclude_extra=Session.dsms.config.hide_properties, + ) + for connection in self + ] + ) + + @property + def by_id(self) -> dict[str, User]: + """Return a dictionary of users indexed by their ID.""" + return {user.id: user for user in self} + + @property + def by_username(self) -> dict[str, User]: + """Return a dictionary of users indexed by their username.""" + return {user.username: user for user in self} + + +class BaseGroup(BaseModel): """User Group Model""" id: str = Field(..., description="The unique identifier of the group.") name: str = Field(..., description="The name of the group.") + + +class Group(BaseGroup): + """User Group Model with Subgroups""" + subgroups: Optional[List["Group"]] = Field( None, description="A list of subgroups." ) @@ -33,7 +70,7 @@ def __repr__(self) -> str: return str(self) def __str__(self): - """Pretty print the LinkedKItemList""" + """Pretty print the GroupList""" from dsms.knowledge.utils import dump_model return yaml.dump( @@ -58,7 +95,7 @@ def flat(self) -> List[Group]: def _flatten(groups: List[Group]): for group in groups: flat_list.append( - Group(**group.model_dump(exclude={"subgroups"})) + BaseGroup(**group.model_dump(exclude={"subgroups"})) ) if group.subgroups: _flatten(group.subgroups) @@ -66,9 +103,19 @@ def _flatten(groups: List[Group]): _flatten(self) return GroupListBase(flat_list) + @property + def by_id(self) -> dict[str, Group]: + """Return a dictionary of groups indexed by their ID.""" + return {group.id: group for group in self.flat} + + @property + def by_name(self) -> dict[str, Group]: + """Return a dictionary of groups indexed by their name.""" + return {group.name: group for group in self.flat} + Group.model_rebuild() interally_public = Group(id="dsms:internally_public", name="Internally Public") externally_public = Group( id="dsms:externally_public", name="Externally Public" -) \ No newline at end of file +) diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py index af72c36..ff77cfb 100644 --- a/dsms/knowledge/groups/public.py +++ b/dsms/knowledge/groups/public.py @@ -1,24 +1,24 @@ """DSMS Public User Groups Module.""" -from .models import Group - -from dsms.core.session import Session from dsms.core.configuration import BaseConfiguration +from dsms.core.session import Session +from .models import Group if not Session.dsms: config = BaseConfiguration() else: config = Session.dsms.config -# The internally/externally public group objects will generally -# be served by the user-service, but we define them here for uniquely +# The internally/externally public group objects will generally +# be served by the user-service, but we define them here for uniquely # setting the ids and names in a common place. # They can be adapted through environment variables anyway. # A common place is needed because the group objects are used in various places # such as internally within the knowledge service, the user service, and the SDK itself. # If the IDs and names of these public groups are only delivered in the user service, -# we cannot distinguish them from the ones which come from keycloak - indicating only organizational groups. +# we cannot distinguish them from the ones which come from keycloak +# - indicating only organizational groups. INTERNALLY_PUBLIC_GROUP = Group( id=config.id_internally_public, diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 5701dff..7a66fbe 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1481,7 +1481,7 @@ def _get_user_groups(dsms: "DSMS"): def _get_user_list(dsms: "DSMS"): """Fetch all users from the DSMS backend.""" - from dsms.knowledge.groups import User + from dsms.knowledge.groups import User, UserList response = _perform_request( dsms, @@ -1491,4 +1491,4 @@ def _get_user_list(dsms: "DSMS"): if not response.ok: raise ConnectionError(f"Failed to fetch users: {response.text}") users = response.json() - return [User(**user) for user in users] + return UserList([User(**user) for user in users]) From a8faf81ce4e68cedc37d57c5759c45f90f8fe011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 9 Dec 2025 18:56:15 +0100 Subject: [PATCH 11/48] update model for user groups --- dsms/knowledge/groups/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 4d7698d..0f5defa 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -13,6 +13,9 @@ class User(BaseModel): id: str = Field(..., description="The unique identifier of the user.") username: str = Field(..., description="The username of the user.") + user_groups: Optional[List["BaseGroup"]] = Field( + None, description="A list of groups the user belongs to." + ) class UserList(list): From fbe543b497d309eaf9ce2d28c920c4a7e8b92812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 9 Dec 2025 19:18:37 +0100 Subject: [PATCH 12/48] add methods for querying individual users --- dsms/knowledge/groups/models.py | 19 +++++++++++++++++++ dsms/knowledge/utils.py | 13 +++++++++++++ 2 files changed, 32 insertions(+) diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 0f5defa..d3c08ed 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -17,6 +17,20 @@ class User(BaseModel): None, description="A list of groups the user belongs to." ) + def __repr__(self) -> str: + """String representation of the GroupList.""" + return str(self) + + def __str__(self): + """Pretty print the User""" + from dsms.knowledge.utils import print_model + + return print_model( + self, + "user", + exclude_extra=Session.dsms.config.hide_properties, + ) + class UserList(list): """List of Users with utility methods.""" @@ -49,6 +63,11 @@ def by_username(self) -> dict[str, User]: """Return a dictionary of users indexed by their username.""" return {user.username: user for user in self} + def __getitem__(self, user_id: str) -> User: + """Get a user by ID""" + + return self.by_id[user_id] + class BaseGroup(BaseModel): """User Group Model""" diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 7a66fbe..f7c3aa1 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1492,3 +1492,16 @@ def _get_user_list(dsms: "DSMS"): raise ConnectionError(f"Failed to fetch users: {response.text}") users = response.json() return UserList([User(**user) for user in users]) + + +def get_user_by_id(user_id: str): + """Fetch a user by ID from the DSMS backend.""" + + response = _perform_request( + Session.dsms, + f"api/users/{user_id}", + "get", + ) + if not response.ok: + raise ValueError(f"Failed to fetch user {user_id}: {response.text}") + return response.json() From 0f4bb6af1ab9dbd42ddb15c7215bc4d3db36674f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 9 Dec 2025 21:47:30 +0100 Subject: [PATCH 13/48] add user id context to kitem list --- dsms/core/dsms.py | 12 +++++++----- dsms/knowledge/utils.py | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index 62f309d..9250e4b 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -3,7 +3,7 @@ import os import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from uuid import UUID from dotenv import load_dotenv @@ -30,8 +30,6 @@ ) if TYPE_CHECKING: - from typing import Optional - from dsms.core.session import Buffers from dsms.knowledge.groups import Group, User from dsms.knowledge.search import KItemListModel, SearchResult @@ -305,7 +303,9 @@ def kitems(self) -> "KItemListModel": warnings.warn(message, DeprecationWarning) return _get_kitem_list(self) - def get_kitems(self, limit=10, offset=0) -> "KItemListModel": + def get_kitems( + self, user_id: Optional[str] = None, limit=10, offset=0 + ) -> "KItemListModel": """ Get all available KItems from the remote backend. @@ -314,7 +314,9 @@ def get_kitems(self, limit=10, offset=0) -> "KItemListModel": offset (int): The offset in the list of KItems. Defaults to 0. """ - return _get_kitem_list(self, limit=limit, offset=offset) + return _get_kitem_list( + self, user_id=user_id, limit=limit, offset=offset + ) @property def app_configs(self) -> "List[AppConfig]": diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index f7c3aa1..896d4cb 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -262,7 +262,9 @@ def _delete_ktype(ktype: "KType") -> None: _get_remote_ktypes(ktype.dsms) -def _get_kitem_list(dsms: "DSMS", limit=10, offset=0) -> "KItemListModel": +def _get_kitem_list( + dsms: "DSMS", user_id: Optional[str] = None, limit=10, offset=0 +) -> "KItemListModel": """Get all available KItems from the remote backend.""" from dsms.knowledge.kitem import KItem # isort:skip @@ -271,6 +273,7 @@ def _get_kitem_list(dsms: "DSMS", limit=10, offset=0) -> "KItemListModel": "api/knowledge/kitems", "get", params={ + "user_id": user_id, "limit": limit, "offset": offset, }, From 56ff5180d281a5a203e24bf04b75889c0cdadbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 11 Dec 2025 17:31:59 +0100 Subject: [PATCH 14/48] update avatar validator --- dsms/knowledge/kitem.py | 11 +++++++++++ dsms/knowledge/utils.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index 66589a3..c1667cb 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -287,6 +287,17 @@ def validate_apps(cls, value: List[App], info: ValidationInfo) -> AppList: app.id = kitem_id return AppList(value) + @field_validator("avatar", mode="after") + @classmethod + def validate_avatar(cls, value: Avatar, info: ValidationInfo) -> Avatar: + """ + Validate avatar Field + """ + kitem_id = info.data.get("id") + if value: + value.id = kitem_id + return value + @field_validator("linked_kitems", mode="before") @classmethod def validate_linked_kitems_list( diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 896d4cb..ebd6482 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1092,9 +1092,9 @@ def _make_avatar( return avatar -def _get_avatar(dsms: "DSMS", kitem_id: UUID) -> Image.Image: +def _get_avatar(kitem_id: UUID) -> Image.Image: response = _perform_request( - dsms, f"api/knowledge/avatar/{kitem_id}", "get" + Session.dsms, f"api/knowledge/avatar/{kitem_id}", "get" ) buffer = io.BytesIO(response.content) return Image.open(buffer) From d1f96e086c105d408e78010cb377a3e50f0ea070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 11 Dec 2025 22:07:49 +0100 Subject: [PATCH 15/48] access properties validators --- dsms/knowledge/kitem.py | 11 +++++------ dsms/knowledge/properties/access.py | 2 +- tests/test_utils.py | 3 --- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index c1667cb..0cea829 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -44,10 +44,9 @@ KItemRelationshipModel, LinkedKItemsList, Summary, - UserGroup, + KItemAccessProperties, ) - from dsms.knowledge.ktype import KType # isort:skip from dsms.knowledge.utils import ( # isort:skip @@ -172,10 +171,6 @@ class KItem(KItemCompactedModel): summary: Optional[Union[str, Summary]] = Field( None, description="Human readable summary text of the KItem." ) - user_groups: List[UserGroup] = Field( - [], - description="User groups able to access the KItem.", - ) custom_properties: Optional[Union[KItemCustomPropertiesModel]] = Field( None, description="Custom properties associated to the KItem" ) @@ -192,6 +187,10 @@ class KItem(KItemCompactedModel): default_factory=Avatar, description="KItem avatar interface" ) + access_properties: Optional[KItemAccessProperties] = Field( + None, description="Access properties of the KItem" + ) + contexts: List[ Union["KItem", KItemCompactedModel, KItemBaseModel] ] = Field( diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index bcbdcc4..1f15759 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -114,7 +114,7 @@ class KItemAccessProperties(BaseModel): description="List of group access properties.", ) - @field_validator("user_access", "group_access", mode="before") + @field_validator("user_access", "group_access", mode="after") @classmethod def check_duplicates(cls, v): """Ensure no duplicate user or group IDs""" diff --git a/tests/test_utils.py b/tests/test_utils.py index e988333..5ab3fe1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -51,7 +51,6 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): name="bar123", ) - user_group = {"name": "private", "group_id": "private_123"} app = {"executable": "foo.exe", "title": "foo"} kitem_old = { @@ -87,7 +86,6 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): }, }, ], - "user_groups": [user_group], "apps": [ { "id": get_mock_kitem_ids[0], @@ -115,7 +113,6 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): } ], linked_kitems=[linked_kitem3], - user_groups=[user_group], apps=[app], ) From 7a5b1e74fa69b008befae76dfdda285565ef454e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 11 Dec 2025 22:38:34 +0100 Subject: [PATCH 16/48] update pytests --- tests/test_access.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_access.py b/tests/test_access.py index 19a85c1..978d2fe 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -360,3 +360,23 @@ def test_case_sensitive_ids(): assert props.user_by_role[Role.OWNER] == ["User1"] assert props.user_by_role[Role.USER] == ["user1"] assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] + + +def test_case_sensitive_ids_dict(): + """Test that IDs are case sensitive (no duplicates if different case)""" + user_access = [ + {"user_id": "User1", "role": 3}, + {"user_id": "user1", "role": 1}, # Different case + {"user_id": "USER1", "role": 2}, # Different case + ] + + # Should not throw an exception + props = KItemAccessProperties(user_access=user_access, group_access=[]) + + assert len(props.user_access) == 3 + assert props.by_user["User1"].role == Role.OWNER + assert props.by_user["user1"].role == Role.USER + assert props.by_user["USER1"].role == Role.CONTRIBUTOR + assert props.user_by_role[Role.OWNER] == ["User1"] + assert props.user_by_role[Role.USER] == ["user1"] + assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] From 6c83e7aca988da6d82cce91a89e5d84a23037b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 11 Dec 2025 23:03:23 +0100 Subject: [PATCH 17/48] improve printing of models --- dsms/knowledge/groups/models.py | 5 +++++ dsms/knowledge/properties/access.py | 8 ++++++++ dsms/knowledge/utils.py | 10 ++-------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index d3c08ed..33532cd 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -63,6 +63,11 @@ def by_username(self) -> dict[str, User]: """Return a dictionary of users indexed by their username.""" return {user.username: user for user in self} + @property + def by_name(self) -> dict[str, User]: + """Return a dictionary of users indexed by their username.""" + return self.by_username + def __getitem__(self, user_id: str) -> User: """Get a user by ID""" diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 1f15759..0d42120 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, Field, field_validator +from dsms.knowledge.utils import dump_model + class OperationType(str, Enum): """Operation Types Enum""" @@ -81,6 +83,12 @@ def access_level(self) -> List[OperationType]: """Set access level based on role""" return RoleMapping.get_operations(self.role) + def __str__(self) -> str: + return dump_model(self) + + def __repr__(self) -> str: + return str(self) + class UserAccessProperty(BaseAccessProperty): """KItem User Access Property Model""" diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index ebd6482..fb31746 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -93,15 +93,9 @@ def dump_model(self, exclude_extra: set = set()) -> Dict[str, Any]: Dict[str, Any]: A dictionary of the model fields with specified exclusions. """ exclude = self.model_config.get("exclude", set()) | exclude_extra - dumped = self.model_dump( - exclude_none=True, - exclude_unset=True, - exclude=exclude, + return self.model_dump( + exclude_none=True, exclude_unset=True, exclude=exclude, mode="json" ) - return { - key: (str(value) if isinstance(value, UUID) else value) - for key, value in dumped.items() - } def print_ktype(self) -> str: From dcbf1f147f53baeca126e28ed3cd6c810624015d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Thu, 11 Dec 2025 23:50:24 +0100 Subject: [PATCH 18/48] update committing of kitems --- dsms/knowledge/properties/access.py | 29 ++++++++++++++++++++++------- dsms/knowledge/utils.py | 6 +----- tests/test_utils.py | 2 -- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 0d42120..9e06d34 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -3,9 +3,10 @@ from enum import Enum, auto from typing import Dict, List, Optional -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_serializer, field_validator -from dsms.knowledge.utils import dump_model +from dsms.core.session import Session +from dsms.knowledge.utils import print_model class OperationType(str, Enum): @@ -83,11 +84,14 @@ def access_level(self) -> List[OperationType]: """Set access level based on role""" return RoleMapping.get_operations(self.role) - def __str__(self) -> str: - return dump_model(self) - - def __repr__(self) -> str: - return str(self) + @field_serializer("role") + def serialize_role_json(self, value: Role, _info): + """Serialize role to JSON""" + if _info.mode == "json": + response = value.name # JSON mode: use name + else: + response = value.value # Python mode: use value + return response class UserAccessProperty(BaseAccessProperty): @@ -122,6 +126,17 @@ class KItemAccessProperties(BaseModel): description="List of group access properties.", ) + def __str__(self) -> str: + """Pretty print the access properties fields""" + return print_model( + self, + "access_properties", + exclude_extra=Session.dsms.config.hide_properties, + ) + + def __repr__(self) -> str: + return str(self) + @field_validator("user_access", "group_access", mode="after") @classmethod def check_duplicates(cls, v): diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index fb31746..c2226f8 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -354,7 +354,6 @@ def _update_kitem(new_kitem: "KItem", old_kitem: "Dict[str, Any]") -> Response: "rdf_exists", "in_backend", "avatar_exists", - "user_groups", "ktype_id", "attachments", "id", @@ -592,11 +591,8 @@ def _get_kitems_diffs(kitem_old: "Dict[str, Any]", kitem_new: "KItem"): differences = {} attributes = [ ("annotations", ("annotations", "link", "unlink")), - ("user_groups", ("user_groups", "add", "remove")), ] - to_compare = kitem_new.model_dump( - include={"annotations", "user_groups", "contexts"} - ) + to_compare = kitem_new.model_dump(include={"annotations", "contexts"}) for name, terms in attributes: to_add_name = terms[0] + "_to_" + terms[1] to_remove_name = terms[0] + "_to_" + terms[2] diff --git a/tests/test_utils.py b/tests/test_utils.py index 5ab3fe1..233caf4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -133,7 +133,6 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): "namespace": "example", } ], - "user_groups_to_add": [], "kitem_apps_to_update": [ { "executable": "foo.exe", @@ -159,7 +158,6 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): "namespace": "example", } ], - "user_groups_to_remove": [], "kitem_apps_to_remove": [ { "executable": "bar.exe", From 0465534dfa98c7b9dbd54329976fbe2750d6bced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 10:12:54 +0100 Subject: [PATCH 19/48] switch to python mode for pretty printing --- dsms/knowledge/properties/access.py | 2 +- dsms/knowledge/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 9e06d34..9a5d471 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -87,7 +87,7 @@ def access_level(self) -> List[OperationType]: @field_serializer("role") def serialize_role_json(self, value: Role, _info): """Serialize role to JSON""" - if _info.mode == "json": + if _info.mode == "python": response = value.name # JSON mode: use name else: response = value.value # Python mode: use value diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index c2226f8..f7b6dea 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -94,7 +94,7 @@ def dump_model(self, exclude_extra: set = set()) -> Dict[str, Any]: """ exclude = self.model_config.get("exclude", set()) | exclude_extra return self.model_dump( - exclude_none=True, exclude_unset=True, exclude=exclude, mode="json" + exclude_none=True, exclude_unset=True, exclude=exclude, mode="python" ) From 913b664a7913e9259299dccce0c6ecd70d0f3dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 10:29:08 +0100 Subject: [PATCH 20/48] update pre-commit config --- .pre-commit-config.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6316dfb..a7b608f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,14 +45,11 @@ repos: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/bandit - rev: 1.7.5 + rev: 1.9.3 hooks: - id: bandit args: ["-r"] files: ^(dsms)/.* - additional_dependencies: - - "pbr==2.0.0" - - setuptools - repo: local hooks: From 475ee50296f6f89335992a6a65ccd105b6187193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 10:47:33 +0100 Subject: [PATCH 21/48] drop python 3.8+3.9 support --- .github/workflows/ci.yml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4cfb56..7bc32a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] steps: diff --git a/setup.cfg b/setup.cfg index 8b031da..f34f13e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ install_requires = rdflib>=6,<7 requests segno>=1.6,<1.7 -python_requires = >=3.8 +python_requires = >=3.10,<=3.15 include_package_data = True [options.entry_points] From 75ec3caccda277fcd2f0241963f92e7bf137369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 10:59:00 +0100 Subject: [PATCH 22/48] remove upper limit of pydantic, drop python 3.15 support for now --- .github/workflows/ci.yml | 2 +- setup.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bc32a5..eba799f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: diff --git a/setup.cfg b/setup.cfg index f34f13e..0b1da63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,14 +25,14 @@ install_requires = lru-cache<1 oyaml==1 pandas>=2,<3 - pydantic>=2,<=2.11.7 + pydantic>=2,<3 pydantic-settings python-dotenv qrcode-artistic>=3,<4 rdflib>=6,<7 requests segno>=1.6,<1.7 -python_requires = >=3.10,<=3.15 +python_requires = >=3.10,<3.15 include_package_data = True [options.entry_points] From 58fdac9b7a6d27b5b63ac40509bd59ce8bd6b3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 11:09:02 +0100 Subject: [PATCH 23/48] update precommit hooks --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7b608f..4c26584 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,13 +26,13 @@ repos: args: [--profile, black, --filter-files] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.3.0 hooks: - id: flake8 args: [--count, --show-source, --statistics, '--ignore', 'E501,E203,W503,E201,E202,E221,E222,E231,E241,E271,E272,E702,E713'] From c48c23a8bb9a67ac8b9c3106ba72c7665ab1aee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 15:25:39 +0100 Subject: [PATCH 24/48] add json mode when serializing --- dsms/knowledge/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index f7b6dea..a3b96e7 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -364,6 +364,7 @@ def _update_kitem(new_kitem: "KItem", old_kitem: "Dict[str, Any]") -> Response: "contexts", }, exclude_defaults=True, + mode="json" ) payload.update( **differences, From ea01ef4d67d10b45391e9a6f67c5844158202c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCschelberger?= Date: Tue, 17 Feb 2026 15:30:35 +0100 Subject: [PATCH 25/48] apply pre-commit hooks --- dsms/knowledge/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index a3b96e7..208e908 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -364,7 +364,7 @@ def _update_kitem(new_kitem: "KItem", old_kitem: "Dict[str, Any]") -> Response: "contexts", }, exclude_defaults=True, - mode="json" + mode="json", ) payload.update( **differences, From e8dc565c2d7569e674a0d88e187eff58b7142818 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Thu, 4 Jun 2026 12:21:31 -0400 Subject: [PATCH 26/48] Add missing widget types to Widget enum; bump version to v5.0.0 --- dsms/knowledge/webform.py | 22 +++++++++++++++------- setup.cfg | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dsms/knowledge/webform.py b/dsms/knowledge/webform.py index 96986eb..37ae50d 100644 --- a/dsms/knowledge/webform.py +++ b/dsms/knowledge/webform.py @@ -41,16 +41,24 @@ class Widget(Enum): """Enum for widgets""" - TEXT = "Text" - FILE = "File" - TEXTAREA = "Textarea" - NUMBER = "Number" - SLIDER = "Slider" + ARRAY_GROUP = "Array group" CHECKBOX = "Checkbox" - SELECT = "Select" - RADIO = "Radio" + DATE = "Date" + DATETIME = "Date-time" + FILE = "File" + KEY_VALUE_PAIRS = "Key-value pairs" KNOWLEDGE_ITEM = "Knowledge item" + LATEX = "LaTeX" MULTI_SELECT = "Multi-select" + NUMBER = "Number" + RADIO = "Radio" + SELECT = "Select" + SLIDER = "Slider" + STAR_RATING = "Star rating" + TEXT = "Text" + TEXTAREA = "Textarea" + URL = "URL" + VOCABULARY_SELECT = "Vocabulary select" class RelationMappingType(Enum): diff --git a/setup.cfg b/setup.cfg index 8b031da..6bd384f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = dsms_sdk -version = v4.1.1 +version = v5.0.0 description = Python SDK core-package for working with the Dataspace Management System (DSMS). long_description = file: README.md long_description_content_type = text/markdown From 0e260f4b3ddb14c3d470cdbfe9460daf59137086 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 06:10:22 -0400 Subject: [PATCH 27/48] Fix access-rights design issues identified in post-merge review - Remove dead-code group objects with underscore IDs (interally_public, externally_public) that conflicted with config defaults (hyphens) - Add refresh_public_groups(config) so DSMS can update the module-level INTERNALLY/EXTERNALLY_PUBLIC_GROUP constants after its own config is set, fixing the import-time staleness bug - Fix min_access_level / max_access_level to return Role (not int) and raise a clear ValueError for operations with no role mapping (e.g. CREATE) - Fix inverted comments in serialize_role_json - Cache user_groups and users on the DSMS instance (pattern matches ktypes); add refresh_user_groups() and refresh_users() invalidation methods - Fix get_user_by_id: accept dsms as first argument (consistent with all other util functions) and return a typed User object instead of a raw dict - Expose get_user_by_id as DSMS.get_user(user_id) --- dsms/core/dsms.py | 45 ++++++++++++++++++++++++++--- dsms/knowledge/groups/__init__.py | 3 +- dsms/knowledge/groups/models.py | 4 --- dsms/knowledge/groups/public.py | 39 ++++++++++++++++--------- dsms/knowledge/properties/access.py | 38 +++++++++++++++--------- dsms/knowledge/utils.py | 17 ++++++++--- 6 files changed, 106 insertions(+), 40 deletions(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index 9250e4b..c635880 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -27,6 +27,7 @@ _get_webform_schemas, _get_user_groups, _get_user_list, + get_user_by_id, ) if TYPE_CHECKING: @@ -81,6 +82,8 @@ def __init__( self._config = None self._ktypes = None + self._user_groups = None + self._users = None self._session.dsms = self if env: @@ -102,6 +105,9 @@ def __init__( an instance of this `Configuration`-object directly.""" ) + from dsms.knowledge.groups.public import refresh_public_groups + refresh_public_groups(self.config) + self._sparql_interface = SparqlInterface(self) if self.config.auto_fetch_ktypes: _get_remote_ktypes(self) @@ -338,13 +344,44 @@ def session(self) -> "Session": @property def user_groups(self) -> "List[Group]": - """Return user groups of the DSMS session""" - return _get_user_groups(self) + """Return user groups, fetching from the backend on first access. + + Results are cached for the lifetime of this DSMS instance. + Call refresh_user_groups() to force a re-fetch. + """ + if self._user_groups is None: + self._user_groups = _get_user_groups(self) + return self._user_groups + + def refresh_user_groups(self) -> None: + """Re-fetch user groups from the backend and update the local cache.""" + self._user_groups = _get_user_groups(self) @property def users(self) -> "List[User]": - """Return user list of the DSMS session""" - return _get_user_list(self) + """Return all users, fetching from the backend on first access. + + Results are cached for the lifetime of this DSMS instance. + Call refresh_users() to force a re-fetch. + """ + if self._users is None: + self._users = _get_user_list(self) + return self._users + + def refresh_users(self) -> None: + """Re-fetch users from the backend and update the local cache.""" + self._users = _get_user_list(self) + + def get_user(self, user_id: str) -> "User": + """Fetch a single user by ID from the backend. + + Args: + user_id: The unique identifier of the user. + + Returns: + User object for the given ID. + """ + return get_user_by_id(self, user_id) @classmethod def __get_pydantic_core_schema__(cls): diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index f027c21..98af133 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -1,7 +1,7 @@ """DSMS User Groups Module.""" from .models import BaseGroup, Group, GroupList, GroupListBase, User, UserList -from .public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP +from .public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP, refresh_public_groups __all__ = [ "Group", @@ -9,6 +9,7 @@ "GroupListBase", "INTERNALLY_PUBLIC_GROUP", "EXTERNALLY_PUBLIC_GROUP", + "refresh_public_groups", "User", "BaseGroup", "UserList", diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 33532cd..7afae34 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -142,7 +142,3 @@ def by_name(self) -> dict[str, Group]: Group.model_rebuild() -interally_public = Group(id="dsms:internally_public", name="Internally Public") -externally_public = Group( - id="dsms:externally_public", name="Externally Public" -) diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py index ff77cfb..4dd0b5d 100644 --- a/dsms/knowledge/groups/public.py +++ b/dsms/knowledge/groups/public.py @@ -5,11 +5,6 @@ from .models import Group -if not Session.dsms: - config = BaseConfiguration() -else: - config = Session.dsms.config - # The internally/externally public group objects will generally # be served by the user-service, but we define them here for uniquely # setting the ids and names in a common place. @@ -19,13 +14,31 @@ # If the IDs and names of these public groups are only delivered in the user service, # we cannot distinguish them from the ones which come from keycloak # - indicating only organizational groups. +# +# NOTE: These constants are initialised at import time using whatever config is +# available then (env-vars or defaults). If a DSMS instance is later created +# with a Configuration that overrides id_internally_public / id_externally_public, +# call refresh_public_groups(config) to keep the constants in sync. + + +def _make_public_groups(cfg=None): + if cfg is None: + cfg = Session.dsms.config if Session.dsms else BaseConfiguration() + return ( + Group(id=cfg.id_internally_public, name=cfg.label_internally_public), + Group(id=cfg.id_externally_public, name=cfg.label_externally_public), + ) + + +INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups() + -INTERNALLY_PUBLIC_GROUP = Group( - id=config.id_internally_public, - name=config.label_internally_public, -) +def refresh_public_groups(config=None) -> None: + """Re-create the public group constants from the given (or current) config. -EXTERNALLY_PUBLIC_GROUP = Group( - id=config.id_externally_public, - name=config.label_externally_public, -) + Call this after constructing a DSMS instance whose Configuration overrides + id_internally_public or id_externally_public so that the module-level + constants stay in sync with the running configuration. + """ + global INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP + INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups(config) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 9a5d471..8b7a5e2 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -53,21 +53,31 @@ def get_operations(cls, role: Role) -> List[OperationType]: @classmethod def min_access_level(cls, operation: OperationType) -> Role: - """Get minimum role required for an operation""" - return min( - role.value - for role in Role - if operation in cls.get_operations(role) - ) + """Get minimum role required for an operation. + + Raises ValueError if no role grants the given operation (e.g. CREATE). + """ + candidates = [role.value for role in Role if operation in cls.get_operations(role)] + if not candidates: + raise ValueError( + f"No role grants the '{operation.value}' operation. " + f"Valid operations are: {[op.value for op in OperationType if any(op in cls.get_operations(r) for r in Role)]}" + ) + return Role(min(candidates)) @classmethod def max_access_level(cls, operation: OperationType) -> Role: - """Get maximum role required for an operation""" - return max( - role.value - for role in Role - if operation in cls.get_operations(role) - ) + """Get maximum role required for an operation. + + Raises ValueError if no role grants the given operation (e.g. CREATE). + """ + candidates = [role.value for role in Role if operation in cls.get_operations(role)] + if not candidates: + raise ValueError( + f"No role grants the '{operation.value}' operation. " + f"Valid operations are: {[op.value for op in OperationType if any(op in cls.get_operations(r) for r in Role)]}" + ) + return Role(max(candidates)) class BaseAccessProperty(BaseModel): @@ -88,9 +98,9 @@ def access_level(self) -> List[OperationType]: def serialize_role_json(self, value: Role, _info): """Serialize role to JSON""" if _info.mode == "python": - response = value.name # JSON mode: use name + response = value.name # Python mode: use human-readable name else: - response = value.value # Python mode: use value + response = value.value # JSON/wire mode: use integer value return response diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 208e908..297e92e 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1488,14 +1488,23 @@ def _get_user_list(dsms: "DSMS"): return UserList([User(**user) for user in users]) -def get_user_by_id(user_id: str): - """Fetch a user by ID from the DSMS backend.""" +def get_user_by_id(dsms: "DSMS", user_id: str) -> "User": + """Fetch a single user by ID from the DSMS backend. + + Args: + dsms: The DSMS instance to use for the request. + user_id: The unique identifier of the user. + + Returns: + User object for the given ID. + """ + from dsms.knowledge.groups import User response = _perform_request( - Session.dsms, + dsms, f"api/users/{user_id}", "get", ) if not response.ok: raise ValueError(f"Failed to fetch user {user_id}: {response.text}") - return response.json() + return User(**response.json()) From e563a404e76c37f972b6f403dd596650e29d440b Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 06:20:08 -0400 Subject: [PATCH 28/48] Add unit tests for access-rights and groups modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_access_extended.py: - Role hierarchy ordering and >= comparison - min/max_access_level return Role instances (not int) - min/max_access_level raise ValueError for unmapped operations (CREATE) - Error message lists valid operations - serialize_role_json: JSON mode → int, Python mode → name string - model_dump(mode='json') produces integer roles for wire format - model_dump(mode='python') produces string role names - Round-trip from backend dict (int roles) through model and back - user_by_role property - None user_access/group_access converted to [] test_groups.py: - Group model: basic, subgroups, deeply nested - GroupList: flat flattening, flat returns BaseGroup, by_id, by_name (including subgroup traversal) - User model: basic, with groups - UserList: by_id, by_username, by_name, __getitem__, missing key - INTERNALLY/EXTERNALLY_PUBLIC_GROUP IDs use hyphens - refresh_public_groups with custom config and without arg - DSMS.user_groups caches result; refresh_user_groups() re-fetches - DSMS.users caches result; refresh_users() re-fetches - DSMS.get_user() returns typed User object; raises on 404 --- tests/test_access_extended.py | 210 +++++++++++++++++++ tests/test_groups.py | 369 ++++++++++++++++++++++++++++++++++ 2 files changed, 579 insertions(+) create mode 100644 tests/test_access_extended.py create mode 100644 tests/test_groups.py diff --git a/tests/test_access_extended.py b/tests/test_access_extended.py new file mode 100644 index 0000000..eb9ff11 --- /dev/null +++ b/tests/test_access_extended.py @@ -0,0 +1,210 @@ +"""Extended tests for access.py — covering gaps identified in post-merge review.""" + +import pytest + +from dsms.knowledge.properties.access import ( + GroupAccessProperty, + KItemAccessProperties, + OperationType, + Role, + RoleMapping, + UserAccessProperty, +) + + +# --------------------------------------------------------------------------- +# Role hierarchy +# --------------------------------------------------------------------------- + + +def test_role_ordering(): + """Role integer values must be strictly ascending: USER < CONTRIBUTOR < OWNER < ADMIN.""" + assert Role.USER < Role.CONTRIBUTOR < Role.OWNER < Role.ADMIN + + +def test_role_gte_comparison(): + """>= on Role values must work correctly for threshold checks.""" + assert Role.OWNER >= Role.CONTRIBUTOR + assert Role.ADMIN >= Role.OWNER + assert not (Role.USER >= Role.CONTRIBUTOR) + + +# --------------------------------------------------------------------------- +# min_access_level / max_access_level return type +# --------------------------------------------------------------------------- + + +def test_min_access_level_returns_role_instance(): + """min_access_level must return a Role member, not a plain int.""" + result = RoleMapping.min_access_level(OperationType.READ) + assert isinstance(result, Role) + assert result is Role.USER + + +def test_max_access_level_returns_role_instance(): + """max_access_level must return a Role member, not a plain int.""" + result = RoleMapping.max_access_level(OperationType.READ) + assert isinstance(result, Role) + assert result is Role.ADMIN + + +@pytest.mark.parametrize( + "operation, expected_min", + [ + (OperationType.READ, Role.USER), + (OperationType.UPDATE, Role.CONTRIBUTOR), + (OperationType.DELETE, Role.OWNER), + (OperationType.MANAGE, Role.OWNER), + ], +) +def test_min_access_level_correct_role(operation, expected_min): + assert RoleMapping.min_access_level(operation) is expected_min + + +@pytest.mark.parametrize( + "operation", + [ + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ], +) +def test_max_access_level_is_admin(operation): + """ADMIN always holds the maximum access level for every mapped operation.""" + assert RoleMapping.max_access_level(operation) is Role.ADMIN + + +# --------------------------------------------------------------------------- +# Unmapped operations raise ValueError +# --------------------------------------------------------------------------- + + +def test_min_access_level_create_raises(): + """CREATE is not assigned to any role — min_access_level must raise ValueError.""" + with pytest.raises(ValueError, match="create"): + RoleMapping.min_access_level(OperationType.CREATE) + + +def test_max_access_level_create_raises(): + """CREATE is not assigned to any role — max_access_level must raise ValueError.""" + with pytest.raises(ValueError, match="create"): + RoleMapping.max_access_level(OperationType.CREATE) + + +def test_error_message_lists_valid_operations(): + """The ValueError message must tell the caller which operations are valid.""" + with pytest.raises(ValueError) as exc_info: + RoleMapping.min_access_level(OperationType.CREATE) + msg = str(exc_info.value) + for op in (OperationType.READ, OperationType.UPDATE, OperationType.DELETE, OperationType.MANAGE): + assert op.value in msg + + +# --------------------------------------------------------------------------- +# serialize_role_json — correct mode behaviour +# --------------------------------------------------------------------------- + + +def test_serialize_role_json_mode(): + """Python mode → name string; JSON/wire mode → integer value.""" + prop = UserAccessProperty(user_id="u1", role=Role.OWNER) + + python_dump = prop.model_dump(mode="python") + assert python_dump["role"] == "OWNER" + assert isinstance(python_dump["role"], str) + + json_dump = prop.model_dump(mode="json") + assert json_dump["role"] == Role.OWNER.value + assert isinstance(json_dump["role"], int) + + +# --------------------------------------------------------------------------- +# KItemAccessProperties wire format +# --------------------------------------------------------------------------- + + +def test_model_dump_json_produces_integer_roles(): + """model_dump(mode='json') must produce integer role values for the wire format.""" + props = KItemAccessProperties( + user_access=[UserAccessProperty(user_id="u1", role=Role.OWNER)], + group_access=[GroupAccessProperty(group_id="g1", role=Role.USER)], + ) + payload = props.model_dump(mode="json") + + assert payload["user_access"][0]["role"] == Role.OWNER.value + assert isinstance(payload["user_access"][0]["role"], int) + assert payload["group_access"][0]["role"] == Role.USER.value + assert isinstance(payload["group_access"][0]["role"], int) + + +def test_model_dump_python_produces_string_roles(): + """model_dump(mode='python') must produce string role names for display.""" + props = KItemAccessProperties( + user_access=[UserAccessProperty(user_id="u1", role=Role.CONTRIBUTOR)], + group_access=[], + ) + payload = props.model_dump(mode="python") + assert payload["user_access"][0]["role"] == "CONTRIBUTOR" + + +def test_round_trip_from_backend_dict(): + """A payload as returned by the backend (integer roles) must round-trip correctly.""" + backend_payload = { + "user_access": [ + {"user_id": "alice", "role": Role.OWNER.value}, + {"user_id": "bob", "role": Role.USER.value}, + ], + "group_access": [ + {"group_id": "dsms:internally-public", "role": Role.USER.value}, + ], + } + + props = KItemAccessProperties(**backend_payload) + + assert props.by_user["alice"].role is Role.OWNER + assert props.by_user["bob"].role is Role.USER + assert props.by_group["dsms:internally-public"].role is Role.USER + + # Serialise back and verify identity + re_serialised = props.model_dump(mode="json") + assert re_serialised["user_access"][0] == {"user_id": "alice", "role": Role.OWNER.value} + assert re_serialised["group_access"][0] == { + "group_id": "dsms:internally-public", + "role": Role.USER.value, + } + + +# --------------------------------------------------------------------------- +# user_by_role property (untested in original suite) +# --------------------------------------------------------------------------- + + +def test_user_by_role(): + """user_by_role must group user IDs by their Role.""" + props = KItemAccessProperties( + user_access=[ + UserAccessProperty(user_id="alice", role=Role.OWNER), + UserAccessProperty(user_id="bob", role=Role.USER), + UserAccessProperty(user_id="carol", role=Role.USER), + ] + ) + by_role = props.user_by_role + assert by_role[Role.OWNER] == ["alice"] + assert set(by_role[Role.USER]) == {"bob", "carol"} + assert Role.CONTRIBUTOR not in by_role + + +def test_user_by_role_empty(): + assert KItemAccessProperties().user_by_role == {} + + +# --------------------------------------------------------------------------- +# Validator: None inputs become empty lists +# --------------------------------------------------------------------------- + + +def test_none_user_access_becomes_empty_list(): + props = KItemAccessProperties(user_access=None, group_access=None) + assert props.user_access == [] + assert props.group_access == [] diff --git a/tests/test_groups.py b/tests/test_groups.py new file mode 100644 index 0000000..74a5b9e --- /dev/null +++ b/tests/test_groups.py @@ -0,0 +1,369 @@ +"""Tests for groups models, public group constants, and DSMS user/group API.""" + +import json +from urllib.parse import urljoin + +import pytest +import responses as responses_lib + +from dsms.knowledge.groups.models import BaseGroup, Group, GroupList, User, UserList +from dsms.knowledge.groups.public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP + + +# --------------------------------------------------------------------------- +# Group model +# --------------------------------------------------------------------------- + + +def test_group_basic(): + g = Group(id="grp-1", name="Engineering") + assert g.id == "grp-1" + assert g.name == "Engineering" + assert g.subgroups is None + + +def test_group_with_subgroups(): + child = Group(id="grp-child", name="Backend") + parent = Group(id="grp-parent", name="Engineering", subgroups=[child]) + assert len(parent.subgroups) == 1 + assert parent.subgroups[0].id == "grp-child" + + +def test_group_deeply_nested(): + leaf = Group(id="leaf", name="Leaf") + mid = Group(id="mid", name="Mid", subgroups=[leaf]) + root = Group(id="root", name="Root", subgroups=[mid]) + assert root.subgroups[0].subgroups[0].id == "leaf" + + +# --------------------------------------------------------------------------- +# GroupList +# --------------------------------------------------------------------------- + + +def test_grouplist_flat_no_subgroups(): + gl = GroupList([Group(id="a", name="A"), Group(id="b", name="B")]) + flat = gl.flat + assert {g.id for g in flat} == {"a", "b"} + + +def test_grouplist_flat_includes_subgroups(): + child = Group(id="child", name="Child") + parent = Group(id="parent", name="Parent", subgroups=[child]) + gl = GroupList([parent]) + flat = gl.flat + assert {g.id for g in flat} == {"parent", "child"} + + +def test_grouplist_flat_deep(): + leaf = Group(id="leaf", name="Leaf") + mid = Group(id="mid", name="Mid", subgroups=[leaf]) + root = Group(id="root", name="Root", subgroups=[mid]) + gl = GroupList([root]) + assert {g.id for g in gl.flat} == {"root", "mid", "leaf"} + + +def test_grouplist_flat_returns_basegroups(): + """flat must return BaseGroup instances (no subgroups field).""" + child = Group(id="child", name="Child") + parent = Group(id="parent", name="Parent", subgroups=[child]) + gl = GroupList([parent]) + for item in gl.flat: + assert isinstance(item, BaseGroup) + + +def test_grouplist_by_id(): + gl = GroupList([Group(id="a", name="Alpha"), Group(id="b", name="Beta")]) + by_id = gl.by_id + assert by_id["a"].name == "Alpha" + assert by_id["b"].name == "Beta" + + +def test_grouplist_by_id_includes_subgroups(): + child = Group(id="child", name="Child") + parent = Group(id="parent", name="Parent", subgroups=[child]) + gl = GroupList([parent]) + assert "child" in gl.by_id + assert "parent" in gl.by_id + + +def test_grouplist_by_name(): + gl = GroupList([Group(id="a", name="Alpha"), Group(id="b", name="Beta")]) + assert gl.by_name["Alpha"].id == "a" + assert gl.by_name["Beta"].id == "b" + + +# --------------------------------------------------------------------------- +# User model +# --------------------------------------------------------------------------- + + +def test_user_basic(): + u = User(id="u-1", username="alice") + assert u.id == "u-1" + assert u.username == "alice" + assert u.user_groups is None + + +def test_user_with_groups(): + g = BaseGroup(id="g-1", name="Engineering") + u = User(id="u-1", username="alice", user_groups=[g]) + assert len(u.user_groups) == 1 + assert u.user_groups[0].id == "g-1" + + +# --------------------------------------------------------------------------- +# UserList +# --------------------------------------------------------------------------- + + +def test_userlist_by_id(): + ul = UserList([User(id="u1", username="alice"), User(id="u2", username="bob")]) + assert ul.by_id["u1"].username == "alice" + assert ul.by_id["u2"].username == "bob" + + +def test_userlist_by_username(): + ul = UserList([User(id="u1", username="alice"), User(id="u2", username="bob")]) + assert ul.by_username["alice"].id == "u1" + + +def test_userlist_by_name_is_alias_for_by_username(): + ul = UserList([User(id="u1", username="alice")]) + assert ul.by_name == ul.by_username + assert ul.by_name["alice"].id == "u1" + + +def test_userlist_getitem_by_id(): + ul = UserList([User(id="u1", username="alice")]) + assert ul["u1"].username == "alice" + + +def test_userlist_getitem_missing_raises(): + ul = UserList([User(id="u1", username="alice")]) + with pytest.raises(KeyError): + _ = ul["nonexistent"] + + +# --------------------------------------------------------------------------- +# Public group constants +# --------------------------------------------------------------------------- + + +def test_internally_public_group_id_uses_hyphen(): + """ID must use hyphens, matching the BaseConfiguration default.""" + assert INTERNALLY_PUBLIC_GROUP.id == "dsms:internally-public" + + +def test_externally_public_group_id_uses_hyphen(): + assert EXTERNALLY_PUBLIC_GROUP.id == "dsms:externally-public" + + +def test_internally_public_group_has_name(): + assert INTERNALLY_PUBLIC_GROUP.name != "" + + +def test_externally_public_group_has_name(): + assert EXTERNALLY_PUBLIC_GROUP.name != "" + + +def test_refresh_public_groups_uses_custom_config(): + """refresh_public_groups(config) must update the module-level constants.""" + from dsms.core.configuration import BaseConfiguration + from dsms.knowledge.groups import public as pub + + original_id = pub.INTERNALLY_PUBLIC_GROUP.id + + custom_cfg = BaseConfiguration( + id_internally_public="custom:internal", + id_externally_public="custom:external", + label_internally_public="Custom Internal", + label_externally_public="Custom External", + ) + pub.refresh_public_groups(custom_cfg) + + assert pub.INTERNALLY_PUBLIC_GROUP.id == "custom:internal" + assert pub.EXTERNALLY_PUBLIC_GROUP.id == "custom:external" + assert pub.INTERNALLY_PUBLIC_GROUP.name == "Custom Internal" + + # Restore defaults so other tests are not affected + pub.refresh_public_groups() + assert pub.INTERNALLY_PUBLIC_GROUP.id == original_id + + +def test_refresh_public_groups_without_arg_restores_defaults(reset_dsms_session): + """refresh_public_groups() with no argument should use env/defaults.""" + from dsms.knowledge.groups import public as pub + + pub.refresh_public_groups() + assert pub.INTERNALLY_PUBLIC_GROUP.id == "dsms:internally-public" + assert pub.EXTERNALLY_PUBLIC_GROUP.id == "dsms:externally-public" + + +# --------------------------------------------------------------------------- +# DSMS.user_groups, DSMS.users, DSMS.get_user — caching and HTTP behaviour +# --------------------------------------------------------------------------- + +MOCK_GROUPS = [ + {"id": "grp-1", "name": "Engineering", "subgroups": []}, + {"id": "grp-2", "name": "Research", "subgroups": [{"id": "grp-3", "name": "ML", "subgroups": []}]}, +] + +MOCK_USERS = [ + {"id": "u-1", "username": "alice", "user_groups": []}, + {"id": "u-2", "username": "bob", "user_groups": []}, +] + +MOCK_SINGLE_USER = {"id": "u-1", "username": "alice", "user_groups": []} + + +@responses_lib.activate +def test_user_groups_returns_grouplist(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups"), + json=MOCK_GROUPS, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + result = dsms.user_groups + assert isinstance(result, GroupList) + assert len(result) == 2 + assert result.by_id["grp-1"].name == "Engineering" + + +@responses_lib.activate +def test_user_groups_is_cached(custom_address): + """Second access to user_groups must not make a second HTTP request.""" + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups"), + json=MOCK_GROUPS, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + _ = dsms.user_groups + _ = dsms.user_groups + + group_calls = [c for c in responses_lib.calls if "api/users/groups" in c.request.url] + assert len(group_calls) == 1 + + +@responses_lib.activate +def test_refresh_user_groups_re_fetches(custom_address): + """refresh_user_groups() must make a new HTTP request and update the cache.""" + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups"), + json=MOCK_GROUPS, + status=200, + ) + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups"), + json=[{"id": "grp-new", "name": "New Group", "subgroups": []}], + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + _ = dsms.user_groups + dsms.refresh_user_groups() + + assert dsms.user_groups.by_id["grp-new"].name == "New Group" + group_calls = [c for c in responses_lib.calls if "api/users/groups" in c.request.url] + assert len(group_calls) == 2 + + +@responses_lib.activate +def test_users_is_cached(custom_address): + """Second access to users must not make a second HTTP request.""" + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/"), + json=MOCK_USERS, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + _ = dsms.users + _ = dsms.users + + user_calls = [c for c in responses_lib.calls if "api/users/" in c.request.url] + assert len(user_calls) == 1 + + +@responses_lib.activate +def test_refresh_users_re_fetches(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/"), + json=MOCK_USERS, + status=200, + ) + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/"), + json=[{"id": "u-3", "username": "carol", "user_groups": []}], + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + _ = dsms.users + dsms.refresh_users() + + assert dsms.users["u-3"].username == "carol" + + +@responses_lib.activate +def test_get_user_returns_user_object(custom_address): + """DSMS.get_user(id) must return a typed User, not a raw dict.""" + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/u-1"), + json=MOCK_SINGLE_USER, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + user = dsms.get_user("u-1") + + assert isinstance(user, User) + assert user.id == "u-1" + assert user.username == "alice" + + +@responses_lib.activate +def test_get_user_raises_on_missing(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/nonexistent"), + json={"detail": "Not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + with pytest.raises(ValueError, match="nonexistent"): + dsms.get_user("nonexistent") From 9a14dd282584c6995ec68fd9b48faa85d37b9f69 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 06:40:07 -0400 Subject: [PATCH 29/48] apply pre-commit hooks --- .pre-commit-config.yaml | 4 +- .pylintrc | 4 +- dsms/core/dsms.py | 1 + dsms/knowledge/groups/__init__.py | 6 +- dsms/knowledge/groups/public.py | 4 +- dsms/knowledge/properties/access.py | 26 ++++++-- tests/test_access_extended.py | 13 +++- tests/test_groups.py | 94 +++++++++++++++++++++++------ 8 files changed, 122 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c26584..96013ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: rev: 5.12.0 hooks: - id: isort - args: [--profile, black, --filter-files] + args: [--profile, black, --filter-files, --line-length, "79"] - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 @@ -56,7 +56,7 @@ repos: - id: pylint name: pylint entry: pylint - args: ["--rcfile=.pylintrc", "--extension-pkg-whitelist='pydantic'"] + args: ["--rcfile=.pylintrc", "--extension-pkg-whitelist=pydantic"] language: python types: [python] require_serial: true diff --git a/.pylintrc b/.pylintrc index a17714e..19d6b06 100644 --- a/.pylintrc +++ b/.pylintrc @@ -72,6 +72,7 @@ disable= no-name-in-module, cyclic-import, import-outside-toplevel, too-many-arguments, + too-many-positional-arguments, too-many-nested-blocks, dangerous-default-value, too-many-public-methods, @@ -80,6 +81,7 @@ disable= no-name-in-module, too-many-branches, too-many-lines, too-many-statements, + global-statement, @@ -245,7 +247,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. -ignored-modules= +ignored-modules=dotenv,pydantic # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index c635880..4c044af 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -106,6 +106,7 @@ def __init__( ) from dsms.knowledge.groups.public import refresh_public_groups + refresh_public_groups(self.config) self._sparql_interface = SparqlInterface(self) diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index 98af133..05bf012 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -1,7 +1,11 @@ """DSMS User Groups Module.""" from .models import BaseGroup, Group, GroupList, GroupListBase, User, UserList -from .public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP, refresh_public_groups +from .public import ( + EXTERNALLY_PUBLIC_GROUP, + INTERNALLY_PUBLIC_GROUP, + refresh_public_groups, +) __all__ = [ "Group", diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py index 4dd0b5d..03541f4 100644 --- a/dsms/knowledge/groups/public.py +++ b/dsms/knowledge/groups/public.py @@ -41,4 +41,6 @@ def refresh_public_groups(config=None) -> None: constants stay in sync with the running configuration. """ global INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP - INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups(config) + INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups( + config + ) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 8b7a5e2..f67109e 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -57,11 +57,20 @@ def min_access_level(cls, operation: OperationType) -> Role: Raises ValueError if no role grants the given operation (e.g. CREATE). """ - candidates = [role.value for role in Role if operation in cls.get_operations(role)] + candidates = [ + role.value + for role in Role + if operation in cls.get_operations(role) + ] if not candidates: + valid = [ + op.value + for op in OperationType + if any(op in cls.get_operations(r) for r in Role) + ] raise ValueError( f"No role grants the '{operation.value}' operation. " - f"Valid operations are: {[op.value for op in OperationType if any(op in cls.get_operations(r) for r in Role)]}" + f"Valid operations are: {valid}" ) return Role(min(candidates)) @@ -71,11 +80,20 @@ def max_access_level(cls, operation: OperationType) -> Role: Raises ValueError if no role grants the given operation (e.g. CREATE). """ - candidates = [role.value for role in Role if operation in cls.get_operations(role)] + candidates = [ + role.value + for role in Role + if operation in cls.get_operations(role) + ] if not candidates: + valid = [ + op.value + for op in OperationType + if any(op in cls.get_operations(r) for r in Role) + ] raise ValueError( f"No role grants the '{operation.value}' operation. " - f"Valid operations are: {[op.value for op in OperationType if any(op in cls.get_operations(r) for r in Role)]}" + f"Valid operations are: {valid}" ) return Role(max(candidates)) diff --git a/tests/test_access_extended.py b/tests/test_access_extended.py index eb9ff11..5c4c19c 100644 --- a/tests/test_access_extended.py +++ b/tests/test_access_extended.py @@ -11,7 +11,6 @@ UserAccessProperty, ) - # --------------------------------------------------------------------------- # Role hierarchy # --------------------------------------------------------------------------- @@ -97,7 +96,12 @@ def test_error_message_lists_valid_operations(): with pytest.raises(ValueError) as exc_info: RoleMapping.min_access_level(OperationType.CREATE) msg = str(exc_info.value) - for op in (OperationType.READ, OperationType.UPDATE, OperationType.DELETE, OperationType.MANAGE): + for op in ( + OperationType.READ, + OperationType.UPDATE, + OperationType.DELETE, + OperationType.MANAGE, + ): assert op.value in msg @@ -168,7 +172,10 @@ def test_round_trip_from_backend_dict(): # Serialise back and verify identity re_serialised = props.model_dump(mode="json") - assert re_serialised["user_access"][0] == {"user_id": "alice", "role": Role.OWNER.value} + assert re_serialised["user_access"][0] == { + "user_id": "alice", + "role": Role.OWNER.value, + } assert re_serialised["group_access"][0] == { "group_id": "dsms:internally-public", "role": Role.USER.value, diff --git a/tests/test_groups.py b/tests/test_groups.py index 74a5b9e..52dbcd4 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -1,14 +1,21 @@ """Tests for groups models, public group constants, and DSMS user/group API.""" -import json from urllib.parse import urljoin import pytest import responses as responses_lib -from dsms.knowledge.groups.models import BaseGroup, Group, GroupList, User, UserList -from dsms.knowledge.groups.public import EXTERNALLY_PUBLIC_GROUP, INTERNALLY_PUBLIC_GROUP - +from dsms.knowledge.groups.models import ( + BaseGroup, + Group, + GroupList, + User, + UserList, +) +from dsms.knowledge.groups.public import ( + EXTERNALLY_PUBLIC_GROUP, + INTERNALLY_PUBLIC_GROUP, +) # --------------------------------------------------------------------------- # Group model @@ -118,13 +125,17 @@ def test_user_with_groups(): def test_userlist_by_id(): - ul = UserList([User(id="u1", username="alice"), User(id="u2", username="bob")]) + ul = UserList( + [User(id="u1", username="alice"), User(id="u2", username="bob")] + ) assert ul.by_id["u1"].username == "alice" assert ul.by_id["u2"].username == "bob" def test_userlist_by_username(): - ul = UserList([User(id="u1", username="alice"), User(id="u2", username="bob")]) + ul = UserList( + [User(id="u1", username="alice"), User(id="u2", username="bob")] + ) assert ul.by_username["alice"].id == "u1" @@ -191,7 +202,9 @@ def test_refresh_public_groups_uses_custom_config(): assert pub.INTERNALLY_PUBLIC_GROUP.id == original_id -def test_refresh_public_groups_without_arg_restores_defaults(reset_dsms_session): +def test_refresh_public_groups_without_arg_restores_defaults( + reset_dsms_session, +): """refresh_public_groups() with no argument should use env/defaults.""" from dsms.knowledge.groups import public as pub @@ -206,7 +219,11 @@ def test_refresh_public_groups_without_arg_restores_defaults(reset_dsms_session) MOCK_GROUPS = [ {"id": "grp-1", "name": "Engineering", "subgroups": []}, - {"id": "grp-2", "name": "Research", "subgroups": [{"id": "grp-3", "name": "ML", "subgroups": []}]}, + { + "id": "grp-2", + "name": "Research", + "subgroups": [{"id": "grp-3", "name": "ML", "subgroups": []}], + }, ] MOCK_USERS = [ @@ -228,7 +245,12 @@ def test_user_groups_returns_grouplist(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) result = dsms.user_groups assert isinstance(result, GroupList) @@ -248,12 +270,19 @@ def test_user_groups_is_cached(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) _ = dsms.user_groups _ = dsms.user_groups - group_calls = [c for c in responses_lib.calls if "api/users/groups" in c.request.url] + group_calls = [ + c for c in responses_lib.calls if "api/users/groups" in c.request.url + ] assert len(group_calls) == 1 @@ -275,13 +304,20 @@ def test_refresh_user_groups_re_fetches(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) _ = dsms.user_groups dsms.refresh_user_groups() assert dsms.user_groups.by_id["grp-new"].name == "New Group" - group_calls = [c for c in responses_lib.calls if "api/users/groups" in c.request.url] + group_calls = [ + c for c in responses_lib.calls if "api/users/groups" in c.request.url + ] assert len(group_calls) == 2 @@ -297,12 +333,19 @@ def test_users_is_cached(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) _ = dsms.users _ = dsms.users - user_calls = [c for c in responses_lib.calls if "api/users/" in c.request.url] + user_calls = [ + c for c in responses_lib.calls if "api/users/" in c.request.url + ] assert len(user_calls) == 1 @@ -323,7 +366,12 @@ def test_refresh_users_re_fetches(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) _ = dsms.users dsms.refresh_users() @@ -343,7 +391,12 @@ def test_get_user_returns_user_object(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) user = dsms.get_user("u-1") @@ -363,7 +416,12 @@ def test_get_user_raises_on_missing(custom_address): with pytest.warns(UserWarning): from dsms.core.dsms import DSMS - dsms = DSMS(host_url=custom_address, ping_backend=False, auto_fetch_ktypes=False) + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) with pytest.raises(ValueError, match="nonexistent"): dsms.get_user("nonexistent") From 89445e718c6c6b3fc5b674d8b086cfeada806ae7 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 06:44:36 -0400 Subject: [PATCH 30/48] upgrade pre-commit hooks and fix resulting lint issues --- .pre-commit-config.yaml | 10 ++-- .pylintrc | 2 +- dsms/apps/config.py | 2 +- dsms/core/__init__.py | 4 +- dsms/core/dsms.py | 12 ++--- dsms/core/utils.py | 1 + dsms/knowledge/cli.py | 1 - dsms/knowledge/kitem.py | 40 +++++++------- dsms/knowledge/properties/apps.py | 6 +-- dsms/knowledge/properties/contacts.py | 1 - dsms/knowledge/properties/dataframe.py | 1 + dsms/knowledge/properties/linked_kitems.py | 1 - dsms/knowledge/semantics/units/utils.py | 12 ++--- .../sparql_interface/sparql_interface.py | 1 - dsms/knowledge/sparql_interface/utils.py | 1 + dsms/knowledge/utils.py | 52 +++++++++---------- dsms/knowledge/webform.py | 7 +-- setup.cfg | 1 - setup.py | 1 + tests/conftest.py | 1 + tests/test_access.py | 2 +- tests/test_kitem.py | 1 + 22 files changed, 74 insertions(+), 86 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96013ec..fd6fa5c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-json - id: check-yaml @@ -14,13 +14,13 @@ repos: - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 26.5.1 hooks: - id: black args: [--line-length, "79"] - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 8.0.1 hooks: - id: isort args: [--profile, black, --filter-files, --line-length, "79"] @@ -40,12 +40,12 @@ repos: log_file: flake8.log - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 + rev: v3.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit args: ["-r"] diff --git a/.pylintrc b/.pylintrc index 19d6b06..36163aa 100644 --- a/.pylintrc +++ b/.pylintrc @@ -247,7 +247,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. -ignored-modules=dotenv,pydantic +ignored-modules=dotenv,pydantic,pydantic_settings,yaml,oyaml,requests,click,pandas,rdflib,PIL,segno # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/dsms/apps/config.py b/dsms/apps/config.py index 42276b6..0da9a51 100644 --- a/dsms/apps/config.py +++ b/dsms/apps/config.py @@ -1,4 +1,5 @@ """DSMS app models""" + import logging import urllib.parse import warnings @@ -20,7 +21,6 @@ from dsms.core.session import Session # isort:skip - logger = logging.getLogger(__name__) logger.addHandler(handler) logger.propagate = False diff --git a/dsms/core/__init__.py b/dsms/core/__init__.py index 68a7e0c..d95d2c0 100644 --- a/dsms/core/__init__.py +++ b/dsms/core/__init__.py @@ -1,5 +1,5 @@ -"""DSMS core module -""" +"""DSMS core module""" + from dsms.core.configuration import Configuration __all__ = ["Configuration"] diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index 4c044af..4f2da97 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -98,12 +98,10 @@ def __init__( elif config is None: self.config = Configuration(**kwargs) else: - raise ValueError( - """`config`-keyword is defined among others. + raise ValueError("""`config`-keyword is defined among others. The `config`-keyword is reserved for passing a config-object directly. Please specify kwargs for to be passed to the `Configuration`-object _OR_ - an instance of this `Configuration`-object directly.""" - ) + an instance of this `Configuration`-object directly.""") from dsms.knowledge.groups.public import refresh_public_groups @@ -393,10 +391,8 @@ def __get_pydantic_core_schema__(cls): def verify_connection(dsms: DSMS) -> None: """Check if DSMS is valid.""" if not isinstance(dsms, DSMS): - raise TypeError( - f"""The passed object for the dsms-connection - is not of type {DSMS}.""" - ) + raise TypeError(f"""The passed object for the dsms-connection + is not of type {DSMS}.""") if dsms.config.ping_backend: try: response = _ping_backend(dsms) diff --git a/dsms/core/utils.py b/dsms/core/utils.py index 38cd3e4..ef7f073 100644 --- a/dsms/core/utils.py +++ b/dsms/core/utils.py @@ -1,4 +1,5 @@ """Core utils of the DSMS core""" + import json import logging import re diff --git a/dsms/knowledge/cli.py b/dsms/knowledge/cli.py index d52d5fa..d17d815 100644 --- a/dsms/knowledge/cli.py +++ b/dsms/knowledge/cli.py @@ -1,6 +1,5 @@ """Command line interface for kitem inspection""" - import click from dsms import DSMS diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index 0cea829..d7aa053 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -191,11 +191,11 @@ class KItem(KItemCompactedModel): None, description="Access properties of the KItem" ) - contexts: List[ - Union["KItem", KItemCompactedModel, KItemBaseModel] - ] = Field( - [], - description="Contextualized KItems related to this one.", + contexts: List[Union["KItem", KItemCompactedModel, KItemBaseModel]] = ( + Field( + [], + description="Contextualized KItems related to this one.", + ) ) def __init__(self, **kwargs: "Any") -> None: @@ -237,9 +237,11 @@ def validate_annotations_before( ) -> List[Annotation]: """Validate annotations Field""" return [ - Annotation(**_make_annotation_schema(annotation)) - if isinstance(annotation, str) - else annotation + ( + Annotation(**_make_annotation_schema(annotation)) + if isinstance(annotation, str) + else annotation + ) for annotation in value ] @@ -258,9 +260,11 @@ def validate_attachments_before( ) -> List[Attachment]: """Validate attachments Field""" return [ - Attachment(name=attachment) - if isinstance(attachment, str) - else attachment + ( + Attachment(name=attachment) + if isinstance(attachment, str) + else attachment + ) for attachment in value ] @@ -623,12 +627,8 @@ def validate_custom_property_entry( ) and entry.value is not None ): - error_message = ( - """Value `{}` is not a valid select option. - Valid options are: """ - + str(list(choices.keys())) - + "\n" - ) + error_message = """Value `{}` is not a valid select option. + Valid options are: """ + str(list(choices.keys())) + "\n" if not select_options: raise ValueError( f"Widget of type `{entry.type}` does not have select options." @@ -751,13 +751,11 @@ def validate_custom_property_entry( if is_updated: entry.value = kitems else: - warnings.warn( - """ + warnings.warn(""" Strict validation is disabled. Will not strictly type check the custom properties. This also will take place when values are re-assigned. - """ - ) + """) return entry diff --git a/dsms/knowledge/properties/apps.py b/dsms/knowledge/properties/apps.py index 114bcb2..5ba6208 100644 --- a/dsms/knowledge/properties/apps.py +++ b/dsms/knowledge/properties/apps.py @@ -125,9 +125,9 @@ def run( """ kwargs["kitem_id"] = str(self.id) if expose_sdk_config: - kwargs[ - "access_token" - ] = Session.dsms.config.token.get_secret_value() + kwargs["access_token"] = ( + Session.dsms.config.token.get_secret_value() + ) response = _perform_request( Session.dsms, diff --git a/dsms/knowledge/properties/contacts.py b/dsms/knowledge/properties/contacts.py index 657c56e..864e56e 100644 --- a/dsms/knowledge/properties/contacts.py +++ b/dsms/knowledge/properties/contacts.py @@ -1,6 +1,5 @@ """Contacts property of a KItem""" - from typing import Optional from uuid import UUID diff --git a/dsms/knowledge/properties/dataframe.py b/dsms/knowledge/properties/dataframe.py index 60da15e..b3b96a7 100644 --- a/dsms/knowledge/properties/dataframe.py +++ b/dsms/knowledge/properties/dataframe.py @@ -1,4 +1,5 @@ """DataFrame property of a KItem""" + import logging from typing import TYPE_CHECKING from uuid import UUID diff --git a/dsms/knowledge/properties/linked_kitems.py b/dsms/knowledge/properties/linked_kitems.py index aec1078..c82648a 100644 --- a/dsms/knowledge/properties/linked_kitems.py +++ b/dsms/knowledge/properties/linked_kitems.py @@ -26,7 +26,6 @@ def fetch(self) -> "KItem": class KItemRelationshipModel(BaseModel): - """Data model for a relation between two linked KItems""" is_incoming: bool = Field( diff --git a/dsms/knowledge/semantics/units/utils.py b/dsms/knowledge/semantics/units/utils.py index 66599cf..e2e2a61 100644 --- a/dsms/knowledge/semantics/units/utils.py +++ b/dsms/knowledge/semantics/units/utils.py @@ -116,15 +116,11 @@ def get_property_unit( f"Something went wrong catching the unit for property `{property_name}`." ) from error if len(query.results) == 0: - raise ValueError( - f"""Property `{property_name}` does not own any - unit with respect to the semantics applied.""" - ) + raise ValueError(f"""Property `{property_name}` does not own any + unit with respect to the semantics applied.""") if len(query.results) > 1: - raise ValueError( - f"""Property `{property_name}` owns more than one - unit with respect to the semantics applied.""" - ) + raise ValueError(f"""Property `{property_name}` owns more than one + unit with respect to the semantics applied.""") unit = query.results.pop() else: unit = measurement_unit.model_dump() diff --git a/dsms/knowledge/sparql_interface/sparql_interface.py b/dsms/knowledge/sparql_interface/sparql_interface.py index 7ffff07..2ec77fc 100644 --- a/dsms/knowledge/sparql_interface/sparql_interface.py +++ b/dsms/knowledge/sparql_interface/sparql_interface.py @@ -17,7 +17,6 @@ class SparqlInterface: - """Sparql Interface for the DSMS.""" def __init__(self, dsms): diff --git a/dsms/knowledge/sparql_interface/utils.py b/dsms/knowledge/sparql_interface/utils.py index 08cbab8..2959e3c 100644 --- a/dsms/knowledge/sparql_interface/utils.py +++ b/dsms/knowledge/sparql_interface/utils.py @@ -1,4 +1,5 @@ """Sparql interface utilities for the DSMS""" + import io from typing import TYPE_CHECKING diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 297e92e..d09f40f 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1,4 +1,5 @@ """DSMS knowledge utilities""" + import base64 import io import logging @@ -31,6 +32,7 @@ from dsms.apps import AppConfig from dsms.core.session import Buffers from dsms.knowledge import KItem, KType, ProcessSchema, WebformSchema + from dsms.knowledge.groups import User from dsms.knowledge.properties import Attachment logger = logging.getLogger(__name__) @@ -200,10 +202,8 @@ def _get_ktype( response = _perform_request(dsms, f"api/knowledge-type/{ktype_id}", "get") if response.status_code == 404 and raise_error: - raise ValueError( - f"""KType with the id `{ktype_id}` does not exist in - DSMS-instance `{dsms.config.host_url}`""" - ) + raise ValueError(f"""KType with the id `{ktype_id}` does not exist in + DSMS-instance `{dsms.config.host_url}`""") if not response.ok and raise_error: raise ValueError( f"""An error occured fetching the KType with id `{ktype_id}`: @@ -304,10 +304,8 @@ def _get_kitem( response = _perform_request(dsms, f"api/knowledge/kitems/{uuid}", "get") if response.status_code == 404 and raise_error: - raise ValueError( - f"""KItem with uuid `{uuid}` does not exist in - DSMS-instance `{dsms.config.host_url}`""" - ) + raise ValueError(f"""KItem with uuid `{uuid}` does not exist in + DSMS-instance `{dsms.config.host_url}`""") if not response.ok and raise_error: raise ValueError( f"""An error occured fetching the KItem with uuid `{uuid}`: @@ -439,10 +437,8 @@ def _upload_attachments(kitem: "KItem", attachment: "Attachment") -> None: elif isinstance(attachment.content, bytes): file = io.BytesIO(attachment.content) else: - raise TypeError( - f"""Invalid content type of attachment with name - `{attachment.name}`: {type(attachment.content)}""" - ) + raise TypeError(f"""Invalid content type of attachment with name + `{attachment.name}`: {type(attachment.content)}""") file.name = attachment.name upload_file = {"dataFile": file} response = _perform_request( @@ -930,9 +926,11 @@ def _search( return SearchResult( hits=[ { - "kitem": KItemCompactedModel(**item.get("kitem")) - if compact - else KItem(**item.get("kitem")), + "kitem": ( + KItemCompactedModel(**item.get("kitem")) + if compact + else KItem(**item.get("kitem")) + ), "fuzzy": item.get("fuzzy"), } for item in dumped.get("hits") @@ -1164,7 +1162,7 @@ def _transform_custom_properties_schema(custom_properties: Any, webform: Any): def _transform_from_flat_schema( - custom_properties: Dict[str, Any] + custom_properties: Dict[str, Any], ) -> Dict[str, Any]: return {"sections": [_make_misc_section(custom_properties)]} @@ -1424,16 +1422,18 @@ def generate_mapping(ktype_id: str, webform: dict): "relation_type": relation_mapping_extra.get( "type" ), - "object_type": { - "suffix": "max", - "iri": relation_mapping_extra.get( - "classIri" - ), - **unit, - } - if relation_mapping_extra.get("type") - == "object_property" - else object_type, + "object_type": ( + { + "suffix": "max", + "iri": relation_mapping_extra.get( + "classIri" + ), + **unit, + } + if relation_mapping_extra.get("type") + == "object_property" + else object_type + ), } ) diff --git a/dsms/knowledge/webform.py b/dsms/knowledge/webform.py index 37ae50d..1f0aae9 100644 --- a/dsms/knowledge/webform.py +++ b/dsms/knowledge/webform.py @@ -32,7 +32,6 @@ from dsms.core.logging import handler # isort:skip - logger = logging.getLogger(__name__) logger.addHandler(handler) logger.propagate = False @@ -530,11 +529,9 @@ def __getattr__(self, key) -> Any: f"Section with name `{self.name}` has no attribute '{key}'" ) if len(target) > 1: - raise AttributeError( - f"""Section with name `{self.name}` + raise AttributeError(f"""Section with name `{self.name}` has multiple attributes '{key}'. - Please specify the concrete entry via indexing !""" - ) + Please specify the concrete entry via indexing !""") target = target.pop() else: diff --git a/setup.cfg b/setup.cfg index d104f80..0f83e61 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ license = BSD-3-Clause license_files = LICENSE classifiers = Development Status :: 2 - Pre-Alpha - License :: OSI Approved :: BSD License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython diff --git a/setup.py b/setup.py index 5948248..63e14cb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """This file is required for editable installs of the package.""" + from setuptools import setup setup() diff --git a/tests/conftest.py b/tests/conftest.py index bf894bc..6dfe5d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Conftest for DSMS-SDK""" + import json from typing import TYPE_CHECKING from urllib.parse import urljoin diff --git a/tests/test_access.py b/tests/test_access.py index 978d2fe..589225d 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -1,4 +1,4 @@ -""""Tests for Access Property Module""" +""" "Tests for Access Property Module""" from typing import List diff --git a/tests/test_kitem.py b/tests/test_kitem.py index 0f09770..3f41a9a 100644 --- a/tests/test_kitem.py +++ b/tests/test_kitem.py @@ -1,4 +1,5 @@ """Pytest for basic KItem connection properties""" + import pytest import responses From 436c318f1b423d984736be6646425b5a40ad8827 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 06:50:18 -0400 Subject: [PATCH 31/48] relax overly restrictive dependency version pins in setup.cfg --- setup.cfg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0f83e61..c21f92f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,15 +22,15 @@ install_requires = click>=8,<9 html5lib>=1,<2 lru-cache<1 - oyaml==1 - pandas>=2,<3 + oyaml>=1 + pandas>=2,<4 pydantic>=2,<3 - pydantic-settings + pydantic-settings>=2,<3 python-dotenv qrcode-artistic>=3,<4 - rdflib>=6,<7 + rdflib>=6,<8 requests - segno>=1.6,<1.7 + segno>=1.6,<2 python_requires = >=3.10,<3.15 include_package_data = True From d4300f25494cc683a5776e83f4c6a6f256f795d9 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 07:57:29 -0400 Subject: [PATCH 32/48] Add v2 ktype API, schema-data support, and SPARQL context methods New features: - Full v2 ktype CRUD via _v2_create/get/update/delete/list_ktypes - Remote registry operations: _v2_list_remote_ktypes/schemas/versions, _v2_export/import_ktype, _v2_refresh_ktype, _v2_remote_diff, _v2_restore_stash; exposed as DSMS.get_v2_ktype*, create_v2_ktype, etc. - KTypeV2, KTypeSpec, KTypeSpecPayload, CreateKTypeRequest, RemoteDiffOut, RemoteKTypeSummary, RemoteKTypeVersion, RemoteSchemaInfo models - _get_ktypes_by_parent / DSMS.get_ktypes_by_parent: fetch ktype descendants - schema_data support: KItemSchemaData, KItemSchemaDataList, _get/put/delete_schema_data; diff logic in _update_schema_data; exposed as DSMS.get_schema_data(kitem_id) - SPARQL context queries: SparqlInterface.query_context, graph_context - DSMS.search gains contexts and attachment_extensions parameters - DSMS.get_kitems gains name filter parameter - KItemCompactedModel gains avatar_exists, has_contexts, attachment_extensions fields Fixes: - attachment_extensions guard: if attachment_extensions is not None (consistent with contexts guard above it) - _get_ktypes_by_parent docstring: remove misleading "v1" qualifier - KItemSchemaData added to TYPE_CHECKING imports in dsms.py --- dsms/core/dsms.py | 142 +++++++++- dsms/knowledge/__init__.py | 42 ++- dsms/knowledge/compacted.py | 12 +- dsms/knowledge/kitem.py | 20 +- dsms/knowledge/ktype.py | 229 +++++++++++++++- dsms/knowledge/properties/__init__.py | 6 + dsms/knowledge/properties/schema_data.py | 29 ++ .../sparql_interface/sparql_interface.py | 10 + dsms/knowledge/sparql_interface/utils.py | 48 ++++ dsms/knowledge/utils.py | 253 +++++++++++++++++- 10 files changed, 772 insertions(+), 19 deletions(-) create mode 100644 dsms/knowledge/properties/schema_data.py diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index 4f2da97..c68b5d1 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -14,7 +14,18 @@ from dsms.core.session import Session from dsms.core.utils import _ping_backend from dsms.knowledge.kitem import KItem -from dsms.knowledge.ktype import KType, ProcessSchema +from dsms.knowledge.ktype import ( + CreateKTypeRequest, + KType, + KTypeSpec, + KTypeSpecPayload, + KTypeV2, + ProcessSchema, + RemoteDiffOut, + RemoteKTypeSummary, + RemoteKTypeVersion, + RemoteSchemaInfo, +) from dsms.knowledge.sparql_interface import SparqlInterface from dsms.knowledge.utils import _search @@ -22,17 +33,33 @@ _commit, _get_kitem, _get_kitem_list, + _get_ktypes_by_parent, _get_remote_ktypes, _get_process_schemas, _get_webform_schemas, + _get_schema_data, _get_user_groups, _get_user_list, + _v2_create_ktype, + _v2_delete_ktype, + _v2_export_ktype, + _v2_get_ktype, + _v2_import_ktype, + _v2_list_ktypes, + _v2_list_remote_ktypes, + _v2_list_remote_schemas, + _v2_list_remote_versions, + _v2_refresh_ktype, + _v2_remote_diff, + _v2_restore_stash, + _v2_update_ktype, get_user_by_id, ) if TYPE_CHECKING: from dsms.core.session import Buffers from dsms.knowledge.groups import Group, User + from dsms.knowledge.properties.schema_data import KItemSchemaData from dsms.knowledge.search import KItemListModel, SearchResult @@ -210,6 +237,8 @@ def search( offset: int = 0, allow_fuzzy: "Optional[bool]" = True, compact: "Optional[bool]" = False, + contexts: "Optional[List[str]]" = None, + attachment_extensions: "Optional[List[str]]" = None, ) -> "List[SearchResult]": """Search for KItems in the remote backend.""" return _search( @@ -221,6 +250,8 @@ def search( offset, allow_fuzzy, compact, + contexts, + attachment_extensions, ) @property @@ -309,18 +340,24 @@ def kitems(self) -> "KItemListModel": return _get_kitem_list(self) def get_kitems( - self, user_id: Optional[str] = None, limit=10, offset=0 + self, + user_id: Optional[str] = None, + limit=10, + offset=0, + name: Optional[str] = None, ) -> "KItemListModel": """ Get all available KItems from the remote backend. Args: + user_id (str, optional): Filter by user ID. limit (int): The amount of KItems to be returned. Defaults to 10. offset (int): The offset in the list of KItems. Defaults to 0. + name (str, optional): Filter by KItem name. """ return _get_kitem_list( - self, user_id=user_id, limit=limit, offset=offset + self, user_id=user_id, limit=limit, offset=offset, name=name ) @property @@ -382,6 +419,105 @@ def get_user(self, user_id: str) -> "User": """ return get_user_by_id(self, user_id) + def get_schema_data(self, kitem_id: str) -> "List[KItemSchemaData]": + """Fetch all schema-data entries for a KItem from the remote backend. + + Args: + kitem_id: The unique identifier of the KItem. + + Returns: + List of KItemSchemaData entries. + """ + from dsms.knowledge.properties.schema_data import KItemSchemaData + + return [ + KItemSchemaData(**entry) + for entry in _get_schema_data(self, kitem_id) + ] + + # ------------------------------------------------------------------ + # KType helpers + # ------------------------------------------------------------------ + + def get_ktypes_by_parent(self, parent_id: str) -> List[KType]: + """Return KTypes whose extends chain contains parent_id.""" + return [KType(**kt) for kt in _get_ktypes_by_parent(self, parent_id)] + + # ------------------------------------------------------------------ + # KType v2 API + # ------------------------------------------------------------------ + + def get_v2_ktypes(self) -> List[KTypeV2]: + """List all v2 KTypes (spec=None for v1-only types).""" + return [KTypeV2(**kt) for kt in _v2_list_ktypes(self)] + + def get_v2_ktype(self, ktype_id: str) -> KTypeV2: + """Fetch a single v2 KType by ID.""" + return KTypeV2(**_v2_get_ktype(self, ktype_id)) + + def create_v2_ktype(self, request: CreateKTypeRequest) -> KTypeV2: + """Create or upgrade a KType to v2.""" + return KTypeV2( + **_v2_create_ktype(self, request.model_dump(exclude_none=True)) + ) + + def import_v2_ktype(self, url: str) -> KTypeV2: + """Import a KType spec from a GitHub URL.""" + return KTypeV2(**_v2_import_ktype(self, url)) + + def update_v2_ktype( + self, ktype_id: str, payload: KTypeSpecPayload + ) -> KTypeV2: + """Partially update a v2 KType spec.""" + return KTypeV2( + **_v2_update_ktype( + self, ktype_id, payload.model_dump(exclude_none=True) + ) + ) + + def delete_v2_ktype(self, ktype_id: str) -> None: + """Delete a v2 KType (blocked if KItems exist).""" + _v2_delete_ktype(self, ktype_id) + + def restore_v2_ktype_stash(self, ktype_id: str) -> KTypeV2: + """Restore the pre-import stash for a v2 KType.""" + return KTypeV2(**_v2_restore_stash(self, ktype_id)) + + def refresh_v2_ktype(self, ktype_id: str) -> KTypeV2: + """Re-fetch a v2 KType spec from its stored source URL.""" + return KTypeV2(**_v2_refresh_ktype(self, ktype_id)) + + def export_v2_ktype(self, ktype_id: str) -> str: + """Download a v2 KType spec as YAML text.""" + return _v2_export_ktype(self, ktype_id) + + def get_v2_ktype_spec(self, ktype_id: str) -> Optional[KTypeSpec]: + """Return the KTypeSpec for a v2 KType, or None if not a v2 type.""" + return self.get_v2_ktype(ktype_id).spec + + def list_remote_v2_ktypes(self) -> List[RemoteKTypeSummary]: + """List KTypes available in the remote GitHub repository.""" + return [ + RemoteKTypeSummary(**kt) for kt in _v2_list_remote_ktypes(self) + ] + + def list_remote_schemas(self) -> List[RemoteSchemaInfo]: + """List semantic schemas available in the remote GitHub repository.""" + return [RemoteSchemaInfo(**s) for s in _v2_list_remote_schemas(self)] + + def list_remote_ktype_versions( + self, ktype_id: str + ) -> List[RemoteKTypeVersion]: + """List all GitHub-tagged versions of a v2 KType.""" + return [ + RemoteKTypeVersion(**v) + for v in _v2_list_remote_versions(self, ktype_id) + ] + + def get_v2_ktype_remote_diff(self, ktype_id: str) -> RemoteDiffOut: + """Compare the local v2 KType spec against the latest remote version.""" + return RemoteDiffOut(**_v2_remote_diff(self, ktype_id)) + @classmethod def __get_pydantic_core_schema__(cls): """Get validator of the DSMS-object.""" diff --git a/dsms/knowledge/__init__.py b/dsms/knowledge/__init__.py index 6226e74..2465f97 100644 --- a/dsms/knowledge/__init__.py +++ b/dsms/knowledge/__init__.py @@ -1,12 +1,50 @@ """Knowledge Module of the DSMS""" from dsms.knowledge.kitem import KItem, KItemCompactedModel -from dsms.knowledge.ktype import KType, ProcessSchema, WebformSchema +from dsms.knowledge.ktype import ( + CreateKTypeRequest, + ImportFromUrlRequest, + KType, + KTypeSpec, + KTypeSpecPayload, + KTypeV2, + OntologyClassSpec, + ProcessSchema, + RelationSpec, + RemoteDiffOut, + RemoteKTypeSummary, + RemoteKTypeVersion, + RemoteSchemaInfo, + RemoteSchemaVersionInfo, + SemanticSchemaRef, + SpecDiffField, + WebformSchema, +) +from dsms.knowledge.properties.schema_data import ( + KItemSchemaData, + KItemSchemaDataList, +) __all__ = [ "KItem", - "KType", "KItemCompactedModel", + "KItemSchemaData", + "KItemSchemaDataList", + "KType", + "KTypeSpec", + "KTypeSpecPayload", + "KTypeV2", + "CreateKTypeRequest", + "ImportFromUrlRequest", + "OntologyClassSpec", "ProcessSchema", + "RelationSpec", + "RemoteDiffOut", + "RemoteKTypeSummary", + "RemoteKTypeVersion", + "RemoteSchemaInfo", + "RemoteSchemaVersionInfo", + "SemanticSchemaRef", + "SpecDiffField", "WebformSchema", ] diff --git a/dsms/knowledge/compacted.py b/dsms/knowledge/compacted.py index d615002..af51217 100644 --- a/dsms/knowledge/compacted.py +++ b/dsms/knowledge/compacted.py @@ -1,7 +1,7 @@ """Compacted Knowledge Item implementation of the DSMS""" from enum import Enum -from typing import Optional, Union +from typing import List, Optional, Union from uuid import UUID, uuid4 from pydantic import ( # isort: skip @@ -44,6 +44,16 @@ class KItemCompactedModel(KItemBaseModel): min_length=4, max_length=1000, ) + avatar_exists: Optional[bool] = Field( + False, description="Whether the KItem holds an avatar or not." + ) + has_contexts: bool = Field( + False, description="Whether the KItem belongs to any context." + ) + attachment_extensions: Optional[List[str]] = Field( + None, + description="Unique file extensions present in the KItem's attachments.", + ) def __str__(self) -> str: """Pretty print the kitem fields""" diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index d7aa053..a23e991 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -42,6 +42,7 @@ DataFrameContainer, Column, KItemRelationshipModel, + KItemSchemaData, LinkedKItemsList, Summary, KItemAccessProperties, @@ -144,10 +145,9 @@ class KItem(KItemCompactedModel): description="Affiliations related to a KItem.", ) authors: List[Union[Author, str]] = Field( - [], description="Authorship of the KItem." - ) - avatar_exists: Optional[bool] = Field( - False, description="Whether the KItem holds an avatar or not." + [], + description="Authorship of the KItem. Deprecated: no longer populated by the backend.", + deprecated=True, ) contacts: List[ContactInfo] = Field( [], @@ -180,7 +180,12 @@ class KItem(KItemCompactedModel): ] = Field(None, description="DataFrame interface.") rdf_exists: bool = Field( - False, description="Whether the KItem holds an RDF Graph or not." + False, + description=( + "Whether the KItem holds an RDF Graph or not. " + "Deprecated: no longer populated by the backend." + ), + deprecated=True, ) avatar: Optional[Avatar] = Field( @@ -198,6 +203,11 @@ class KItem(KItemCompactedModel): ) ) + schema_data: Optional[List[KItemSchemaData]] = Field( + None, + description="Semantic schema data entries associated with this KItem.", + ) + def __init__(self, **kwargs: "Any") -> None: """Initialize the KItem""" diff --git a/dsms/knowledge/ktype.py b/dsms/knowledge/ktype.py index 7346cc4..7daa888 100644 --- a/dsms/knowledge/ktype.py +++ b/dsms/knowledge/ktype.py @@ -2,7 +2,7 @@ import logging from datetime import datetime -from typing import TYPE_CHECKING, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from uuid import UUID from pydantic import BaseModel, Field, field_validator @@ -185,3 +185,230 @@ def dsms(self) -> "DSMS": def session(self) -> "Session": """Getter for Session""" return Session + + +# --------------------------------------------------------------------------- +# KType v2 — semantic-spec sub-models +# --------------------------------------------------------------------------- + + +class OntologyClassSpec(BaseModel): + """A single ontology class associated with a KType.""" + + iri: str = Field(..., description="IRI of the ontology class.") + label: str = Field(..., description="Human-readable label.") + ontology: str = Field(..., description="Ontology the class belongs to.") + + +class SemanticSchemaRef(BaseModel): + """Reference to a semantic schema linked to a KType.""" + + id: str = Field(..., description="Schema identifier.") + version: str = Field(..., description="Schema version string.") + url: str = Field(..., description="URL to the schema definition.") + + +class RelationSpec(BaseModel): + """A typed relation defined on a KType.""" + + id: str = Field(..., description="Relation identifier.") + label: str = Field(..., description="Human-readable label.") + description: Optional[str] = Field( + None, description="Optional description." + ) + iri: str = Field(..., description="IRI of the relation property.") + target_k_types: List[str] = Field( + [], description="KType IDs that are valid targets for this relation." + ) + cardinality: str = Field( + "0..n", description="Cardinality string, e.g. '0..n'." + ) + required: bool = Field( + False, description="Whether the relation is required." + ) + + +class KTypeSpec(BaseModel): + """Semantic specification record for a v2 KType (ktype_spec table).""" + + ktype_id: str = Field(..., description="KType ID this spec belongs to.") + format_version: Optional[str] = Field( + None, description="Spec format version, e.g. '0.1'." + ) + spec_id: Optional[str] = Field( + None, description="$id field from the spec YAML." + ) + version: Optional[str] = Field( + None, description="Semver string, e.g. '1.0.0'." + ) + description: Optional[str] = Field(None) + abstract: Optional[bool] = Field( + None, + description="If True the KType cannot be instantiated (no KItems allowed).", + ) + context: Optional[bool] = Field( + None, description="If True the KType can act as a context anchor." + ) + context_member_types: Optional[List[str]] = Field( + None, + description="KType IDs allowed as members of a context of this type.", + ) + synonyms: Optional[List[str]] = Field(None) + extends: Optional[Union[str, List[str]]] = Field( + None, description="Parent KType ID(s) for inheritance." + ) + ontology_classes: Optional[List[OntologyClassSpec]] = Field( + None, description="Ontology classes defined directly on this KType." + ) + resolved_ontology_classes: Optional[List[OntologyClassSpec]] = Field( + None, description="Ontology classes including inherited entries." + ) + semantic_schemas: Optional[List[SemanticSchemaRef]] = Field( + None, + description="Semantic schema references defined directly on this KType.", + ) + resolved_semantic_schemas: Optional[List[SemanticSchemaRef]] = Field( + None, + description="Semantic schema references including inherited entries.", + ) + relations: Optional[List[RelationSpec]] = Field(None) + dynamic_properties: Optional[Dict[str, Any]] = Field( + None, description="Raw webform spec dict (camelCase)." + ) + tags: Optional[List[str]] = Field(None) + source_url: Optional[str] = Field( + None, description="GitHub URL if the spec was imported." + ) + has_stash: bool = Field( + False, description="Whether a pre-import stash exists for this KType." + ) + stashed_spec_version: Optional[str] = Field( + None, description="Version string from the stash, if any." + ) + created_at: Optional[datetime] = Field(None) + updated_at: Optional[datetime] = Field(None) + + +class KTypeV2(KType): + """KType returned by the v2 knowledge-type-service endpoints. + + Extends the base KType with an optional semantic spec. + """ + + spec: Optional[KTypeSpec] = Field( + None, + description="Semantic specification of this KType. None for v1-only KTypes.", + ) + + +# --------------------------------------------------------------------------- +# KType v2 — request models +# --------------------------------------------------------------------------- + + +class CreateKTypeRequest(BaseModel): + """Request body for POST /v2/ktypes/ — create or upgrade a KType.""" + + id: str = Field( + ..., + description="KType ID (slug, e.g. 'my-ktype').", + min_length=2, + max_length=60, + pattern=r"^[a-z][a-z0-9-]*$", + ) + name: str = Field(..., min_length=2, max_length=100) + format_version: str = Field("0.1") + version: str = Field("1.0.0") + description: Optional[str] = Field(None) + abstract: bool = Field(False) + context: bool = Field(False) + context_member_types: Optional[List[str]] = Field(None) + synonyms: Optional[List[str]] = Field(None) + extends: Optional[Union[str, List[str]]] = Field(None) + ontology_classes: Optional[List[OntologyClassSpec]] = Field(None) + semantic_schemas: Optional[List[SemanticSchemaRef]] = Field(None) + relations: Optional[List[RelationSpec]] = Field(None) + dynamic_properties: Optional[Dict[str, Any]] = Field(None) + tags: Optional[List[str]] = Field(None) + + +class ImportFromUrlRequest(BaseModel): + """Request body for POST /v2/ktypes/import — import a spec from a GitHub URL.""" + + url: str = Field(..., description="URL to the raw ktype.yaml on GitHub.") + + +class KTypeSpecPayload(BaseModel): + """Request body for PUT /v2/ktypes/{ktype_id} — partial spec update.""" + + name: Optional[str] = Field(None, min_length=2, max_length=100) + version: Optional[str] = Field(None) + description: Optional[str] = Field(None) + abstract: Optional[bool] = Field(None) + context: Optional[bool] = Field(None) + context_member_types: Optional[List[str]] = Field(None) + synonyms: Optional[List[str]] = Field(None) + extends: Optional[Union[str, List[str]]] = Field(None) + ontology_classes: Optional[List[OntologyClassSpec]] = Field(None) + semantic_schemas: Optional[List[SemanticSchemaRef]] = Field(None) + relations: Optional[List[RelationSpec]] = Field(None) + dynamic_properties: Optional[Dict[str, Any]] = Field(None) + tags: Optional[List[str]] = Field(None) + + +# --------------------------------------------------------------------------- +# KType v2 — response models +# --------------------------------------------------------------------------- + + +class RemoteKTypeSummary(BaseModel): + """Summary of a KType available in the remote GitHub repository.""" + + id: str + name: str + remote_version: str + url: str + status: str = Field( + ..., + description="One of: 'not_imported', 'up_to_date', 'update_available'.", + ) + db_version: Optional[str] = Field(None) + + +class RemoteKTypeVersion(BaseModel): + """A tagged version of a KType in the remote repository.""" + + tag: str + version: str + url: str + + +class RemoteSchemaVersionInfo(BaseModel): + """Version entry for a remote semantic schema.""" + + version: str + url: str + + +class RemoteSchemaInfo(BaseModel): + """A semantic schema available in the remote repository.""" + + id: str + versions: List[RemoteSchemaVersionInfo] = Field([]) + + +class SpecDiffField(BaseModel): + """One changed field in a remote-diff result.""" + + field: str + local: Any + remote: Any + + +class RemoteDiffOut(BaseModel): + """Result of comparing a local KType spec against the latest remote version.""" + + remote_version: str + remote_url: str + changed_fields: List[SpecDiffField] = Field([]) + identical: bool diff --git a/dsms/knowledge/properties/__init__.py b/dsms/knowledge/properties/__init__.py index 6670969..261e6dc 100644 --- a/dsms/knowledge/properties/__init__.py +++ b/dsms/knowledge/properties/__init__.py @@ -31,6 +31,10 @@ ) from dsms.knowledge.properties.avatar import Avatar # isort:skip +from dsms.knowledge.properties.schema_data import ( # isort:skip + KItemSchemaData, + KItemSchemaDataList, +) __all__ = [ "Annotation", @@ -51,4 +55,6 @@ "Column", "KItemRelationshipModel", "KItemAccessProperties", + "KItemSchemaData", + "KItemSchemaDataList", ] diff --git a/dsms/knowledge/properties/schema_data.py b/dsms/knowledge/properties/schema_data.py new file mode 100644 index 0000000..67aa353 --- /dev/null +++ b/dsms/knowledge/properties/schema_data.py @@ -0,0 +1,29 @@ +"""KItem Schema Data property model""" + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class KItemSchemaData(BaseModel): + """Holds the semantic schema data associated with a KItem. + + Each entry maps a schema ID (e.g. an ontology class IRI) to a free-form + content dict that stores the instance data for that schema. + """ + + schema_id: str = Field( + ..., description="Schema identifier (e.g. an ontology class IRI)." + ) + content: Optional[Dict[str, Any]] = Field( + None, description="Instance data for the schema." + ) + + +class KItemSchemaDataList(List[KItemSchemaData]): + """Typed list of KItemSchemaData entries with lookup by schema_id.""" + + @property + def by_schema_id(self) -> "Dict[str, KItemSchemaData]": + """Return a dict keyed by schema_id.""" + return {entry.schema_id: entry for entry in self} diff --git a/dsms/knowledge/sparql_interface/sparql_interface.py b/dsms/knowledge/sparql_interface/sparql_interface.py index 2ec77fc..9e01d79 100644 --- a/dsms/knowledge/sparql_interface/sparql_interface.py +++ b/dsms/knowledge/sparql_interface/sparql_interface.py @@ -6,7 +6,9 @@ from dsms.knowledge.sparql_interface.subgraph import Subgraph from dsms.knowledge.sparql_interface.utils import ( _add_rdf, + _graph_query_context, _sparql_query, + _sparql_query_context, _sparql_update, ) @@ -56,6 +58,14 @@ def insert( repository, ) + def query_context(self, context_id: str, query: str) -> "Dict[str, Any]": + """Perform a SPARQL query scoped to a context KItem.""" + return _sparql_query_context(self._dsms, context_id, query) + + def graph_context(self, context_id: str, query: str) -> "Dict[str, Any]": + """Perform a graph query scoped to a context KItem.""" + return _graph_query_context(self._dsms, context_id, query) + @property def subgraph(self) -> Subgraph: """Subgraph interface for DSMS""" diff --git a/dsms/knowledge/sparql_interface/utils.py b/dsms/knowledge/sparql_interface/utils.py index 2959e3c..dae15db 100644 --- a/dsms/knowledge/sparql_interface/utils.py +++ b/dsms/knowledge/sparql_interface/utils.py @@ -142,6 +142,54 @@ def _update_subgraph( _create_subgraph(dsms, graph, encoding, repository) +def _sparql_query_context( + dsms: "DSMS", context_id: str, query: str +) -> "Dict[str, Any]": + """Submit a SPARQL query scoped to a context KItem.""" + response = _perform_request( + dsms, + "api/knowledge/sparql/context", + "post", + data={"query": query}, + params={"context_id": context_id}, + ) + if not response.ok: + raise RuntimeError( + f"Context SPARQL query was not successful: {response.text}" + ) + try: + response = response.json() + except Exception as excep: + raise RuntimeError( + f"Something went wrong parsing context SPARQL response: `{query}`" + ) from excep + return response + + +def _graph_query_context( + dsms: "DSMS", context_id: str, query: str +) -> "Dict[str, Any]": + """Submit a graph query scoped to a context KItem.""" + response = _perform_request( + dsms, + "api/knowledge/graph/context", + "post", + json={"query": query}, + params={"context_id": context_id}, + ) + if not response.ok: + raise RuntimeError( + f"Context graph query was not successful: {response.text}" + ) + try: + response = response.json() + except Exception as excep: + raise RuntimeError( + f"Something went wrong parsing context graph response: `{query}`" + ) from excep + return response + + def _get_subgraph( dsms: "DSMS", identifier: str, repository: str, is_kitem_id: bool = False ) -> "Graph": diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index d09f40f..8ca1ec9 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -257,20 +257,23 @@ def _delete_ktype(ktype: "KType") -> None: def _get_kitem_list( - dsms: "DSMS", user_id: Optional[str] = None, limit=10, offset=0 + dsms: "DSMS", + user_id: Optional[str] = None, + limit=10, + offset=0, + name: Optional[str] = None, ) -> "KItemListModel": """Get all available KItems from the remote backend.""" from dsms.knowledge.kitem import KItem # isort:skip + params = {"user_id": user_id, "limit": limit, "offset": offset} + if name is not None: + params["name"] = name response = _perform_request( dsms, "api/knowledge/kitems", "get", - params={ - "user_id": user_id, - "limit": limit, - "offset": offset, - }, + params=params, ) if not response.ok: raise ValueError( @@ -611,6 +614,15 @@ def _get_kitems_diffs(kitem_old: "Dict[str, Any]", kitem_new: "KItem"): context_kitems = _get_kitem_contexts(kitem_old, kitem_new) # same holds for kitem apps apps = _get_apps_diff(kitem_old, kitem_new) + # access_properties: include as full replacement when changed + old_access = kitem_old.get("access_properties") + new_access = ( + kitem_new.access_properties.model_dump(mode="json") + if kitem_new.access_properties is not None + else None + ) + if new_access != old_access and new_access is not None: + differences["access_properties"] = new_access # merge with previously found differences differences.update(**linked_kitems, **apps, **context_kitems) return differences @@ -672,6 +684,8 @@ def _commit(buffers: "Buffers") -> None: _delete_dataframe(obj.id) _update_kitem(obj, old_kitem) _update_attachments(obj, old_kitem) + if obj.schema_data is not None: + _update_schema_data(obj, old_kitem) if obj.avatar.file or obj.avatar.encode_qr: _commit_avatar(obj) elif isinstance(obj, KType) or ( @@ -857,6 +871,162 @@ def _refresh_kitem(kitem: "KItem") -> None: kitem.dataframe = [{"id": kitem.id, **column} for column in dataframe] +def _get_ktypes_by_parent( + dsms: "DSMS", parent_id: str +) -> List[Dict[str, Any]]: + """Return KTypes whose spec.extends chain contains parent_id.""" + response = _perform_request( + dsms, "api/knowledge-type/", "get", params={"parent": parent_id} + ) + if not response.ok: + raise ValueError( + f"Failed to fetch ktypes by parent `{parent_id}`: {response.text}" + ) + return response.json() + + +# --------------------------------------------------------------------------- +# KType v2 utilities (api/knowledge-type/v2/ktypes/…) +# --------------------------------------------------------------------------- + +_V2_BASE = "api/knowledge-type/v2/ktypes" + + +def _v2_list_ktypes(dsms: "DSMS") -> List[Dict[str, Any]]: + """GET /v2/ktypes/ — list all v2 KTypes.""" + response = _perform_request(dsms, f"{_V2_BASE}/", "get") + if not response.ok: + raise ValueError(f"Failed to list v2 ktypes: {response.text}") + return response.json() + + +def _v2_get_ktype(dsms: "DSMS", ktype_id: str) -> Dict[str, Any]: + """GET /v2/ktypes/{ktype_id} — fetch a single v2 KType.""" + response = _perform_request(dsms, f"{_V2_BASE}/{ktype_id}", "get") + if not response.ok: + raise ValueError( + f"Failed to fetch v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + +def _v2_create_ktype(dsms: "DSMS", payload: Dict[str, Any]) -> Dict[str, Any]: + """POST /v2/ktypes/ — create or upgrade a v2 KType.""" + response = _perform_request(dsms, f"{_V2_BASE}/", "post", json=payload) + if not response.ok: + raise ValueError(f"Failed to create v2 ktype: {response.text}") + return response.json() + + +def _v2_import_ktype(dsms: "DSMS", url: str) -> Dict[str, Any]: + """POST /v2/ktypes/import — import a KType spec from a GitHub URL.""" + response = _perform_request( + dsms, f"{_V2_BASE}/import", "post", json={"url": url} + ) + if not response.ok: + raise ValueError( + f"Failed to import v2 ktype from `{url}`: {response.text}" + ) + return response.json() + + +def _v2_update_ktype( + dsms: "DSMS", ktype_id: str, payload: Dict[str, Any] +) -> Dict[str, Any]: + """PUT /v2/ktypes/{ktype_id} — partial spec update.""" + response = _perform_request( + dsms, f"{_V2_BASE}/{ktype_id}", "put", json=payload + ) + if not response.ok: + raise ValueError( + f"Failed to update v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + +def _v2_delete_ktype(dsms: "DSMS", ktype_id: str) -> None: + """DELETE /v2/ktypes/{ktype_id} — delete a v2 KType.""" + response = _perform_request(dsms, f"{_V2_BASE}/{ktype_id}", "delete") + if not response.ok: + raise ValueError( + f"Failed to delete v2 ktype `{ktype_id}`: {response.text}" + ) + + +def _v2_restore_stash(dsms: "DSMS", ktype_id: str) -> Dict[str, Any]: + """POST /v2/ktypes/{ktype_id}/restore-stash — restore pre-import stash.""" + response = _perform_request( + dsms, f"{_V2_BASE}/{ktype_id}/restore-stash", "post" + ) + if not response.ok: + raise ValueError( + f"Failed to restore stash for v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + +def _v2_refresh_ktype(dsms: "DSMS", ktype_id: str) -> Dict[str, Any]: + """POST /v2/ktypes/{ktype_id}/refresh — re-fetch spec from source URL.""" + response = _perform_request(dsms, f"{_V2_BASE}/{ktype_id}/refresh", "post") + if not response.ok: + raise ValueError( + f"Failed to refresh v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + +def _v2_export_ktype(dsms: "DSMS", ktype_id: str) -> str: + """GET /v2/ktypes/{ktype_id}/export — download ktype.yaml as text.""" + response = _perform_request(dsms, f"{_V2_BASE}/{ktype_id}/export", "get") + if not response.ok: + raise ValueError( + f"Failed to export v2 ktype `{ktype_id}`: {response.text}" + ) + return response.text + + +def _v2_list_remote_ktypes(dsms: "DSMS") -> List[Dict[str, Any]]: + """GET /v2/ktypes/remote — list KTypes available in the remote repo.""" + response = _perform_request(dsms, f"{_V2_BASE}/remote", "get") + if not response.ok: + raise ValueError(f"Failed to list remote v2 ktypes: {response.text}") + return response.json() + + +def _v2_list_remote_schemas(dsms: "DSMS") -> List[Dict[str, Any]]: + """GET /v2/ktypes/remote/schemas — list semantic schemas in the remote repo.""" + response = _perform_request(dsms, f"{_V2_BASE}/remote/schemas", "get") + if not response.ok: + raise ValueError(f"Failed to list remote schemas: {response.text}") + return response.json() + + +def _v2_list_remote_versions( + dsms: "DSMS", ktype_id: str +) -> List[Dict[str, Any]]: + """GET /v2/ktypes/{ktype_id}/remote-versions — list GitHub tags for a KType.""" + response = _perform_request( + dsms, f"{_V2_BASE}/{ktype_id}/remote-versions", "get" + ) + if not response.ok: + raise ValueError( + f"Failed to list remote versions for v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + +def _v2_remote_diff(dsms: "DSMS", ktype_id: str) -> Dict[str, Any]: + """GET /v2/ktypes/{ktype_id}/remote-diff — diff local vs latest remote spec.""" + response = _perform_request( + dsms, f"{_V2_BASE}/{ktype_id}/remote-diff", "get" + ) + if not response.ok: + raise ValueError( + f"Failed to get remote diff for v2 ktype `{ktype_id}`: {response.text}" + ) + return response.json() + + def _refresh_ktype(ktype: "KType") -> None: """Refresh the KItem""" for key, value in _get_ktype(ktype.dsms, ktype.id, as_json=True).items(): @@ -891,6 +1061,8 @@ def _search( offset: "Optional[int]" = 0, allow_fuzzy: "Optional[bool]" = True, compact: "Optional[bool]" = False, + contexts: "Optional[List[str]]" = None, + attachment_extensions: "Optional[List[str]]" = None, ) -> "List[SearchResult]": """Search for KItems in the remote backend""" from dsms import KItem, KItemCompactedModel @@ -906,12 +1078,17 @@ def _search( "offset": offset, "compact": compact, } + if contexts is not None: + payload["contexts"] = contexts + params = {"allow_fuzzy": allow_fuzzy} + if attachment_extensions is not None: + params["attachment_extensions"] = attachment_extensions response = _perform_request( dsms, "api/knowledge/kitems/search", "post", json=payload, - params={"allow_fuzzy": allow_fuzzy}, + params=params, ) if not response.ok: raise RuntimeError( @@ -1508,3 +1685,65 @@ def get_user_by_id(dsms: "DSMS", user_id: str) -> "User": if not response.ok: raise ValueError(f"Failed to fetch user {user_id}: {response.text}") return User(**response.json()) + + +def _get_schema_data(dsms: "DSMS", kitem_id: str) -> List[Dict[str, Any]]: + """Fetch all schema-data entries for a KItem from the remote backend.""" + response = _perform_request( + dsms, f"api/knowledge/kitems/{kitem_id}/schema-data", "get" + ) + if not response.ok: + raise ValueError( + f"Failed to fetch schema data for kitem `{kitem_id}`: {response.text}" + ) + return response.json() + + +def _put_schema_data( + dsms: "DSMS", + kitem_id: str, + schema_id: str, + content: Optional[Dict[str, Any]], +) -> Dict[str, Any]: + """Create or update a single schema-data entry for a KItem.""" + response = _perform_request( + dsms, + f"api/knowledge/kitems/{kitem_id}/schema-data/{schema_id}", + "put", + json={"content": content}, + ) + if not response.ok: + raise ValueError( + f"Failed to put schema data `{schema_id}` for kitem `{kitem_id}`: {response.text}" + ) + return response.json() + + +def _delete_schema_data(dsms: "DSMS", kitem_id: str, schema_id: str) -> None: + """Delete a single schema-data entry for a KItem.""" + response = _perform_request( + dsms, + f"api/knowledge/kitems/{kitem_id}/schema-data/{schema_id}", + "delete", + ) + if not response.ok: + raise ValueError( + f"Failed to delete schema data `{schema_id}` for kitem `{kitem_id}`: {response.text}" + ) + + +def _update_schema_data(kitem: "KItem", old_kitem: Dict[str, Any]) -> None: + """Sync schema_data changes between local KItem state and the backend.""" + old_entries = { + e["schema_id"]: e.get("content") + for e in old_kitem.get("schema_data", []) + } + new_entries = {e.schema_id: e.content for e in (kitem.schema_data or [])} + + for schema_id, content in new_entries.items(): + if schema_id not in old_entries or old_entries[schema_id] != content: + _put_schema_data(kitem.dsms, str(kitem.id), schema_id, content) + + for schema_id in old_entries: + if schema_id not in new_entries: + _delete_schema_data(kitem.dsms, str(kitem.id), schema_id) From 55a0e8d85b067eb52d36c3976e866c73d31bac68 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 09:37:38 -0400 Subject: [PATCH 33/48] Rename Role.USER to Role.MEMBER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit USER as a role name was ambiguous — everyone is a user of the system. MEMBER better describes the capability: a member of the platform or a group can read the item. The progression MEMBER → CONTRIBUTOR → OWNER → ADMIN now reads as a natural escalation of responsibility. Also resolves the tension with INTERNALLY_PUBLIC_GROUP: assigning member-level access to the internal-public group reads naturally as 'all platform members can access this item'. --- dsms/knowledge/properties/access.py | 4 +-- tests/test_access.py | 54 ++++++++++++++++------------- tests/test_access_extended.py | 28 +++++++-------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index f67109e..216c78d 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -22,7 +22,7 @@ class OperationType(str, Enum): class Role(int, Enum): """Role Enum""" - USER = auto() + MEMBER = auto() CONTRIBUTOR = auto() OWNER = auto() ADMIN = auto() @@ -37,7 +37,7 @@ class RoleMapping(List[OperationType], Enum): OperationType.DELETE, OperationType.MANAGE, ] - USER = [OperationType.READ] + MEMBER = [OperationType.READ] CONTRIBUTOR = [OperationType.READ, OperationType.UPDATE] ADMIN = [ OperationType.READ, diff --git a/tests/test_access.py b/tests/test_access.py index 589225d..e21ec94 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -21,7 +21,7 @@ def sample_user_access() -> List[UserAccessProperty]: """Create sample user access properties""" return [ UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty(user_id="user2", role=Role.MEMBER), UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), ] @@ -31,7 +31,7 @@ def sample_group_access() -> List[GroupAccessProperty]: """Create sample group access properties""" return [ GroupAccessProperty(group_id="group1", role=Role.ADMIN), - GroupAccessProperty(group_id="group2", role=Role.USER), + GroupAccessProperty(group_id="group2", role=Role.MEMBER), ] @@ -61,7 +61,9 @@ def test_access_level_owner(): def test_minimum_access_level(): """Test min_access_level method""" - assert RoleMapping.min_access_level(OperationType.READ) == Role.USER.value + assert ( + RoleMapping.min_access_level(OperationType.READ) == Role.MEMBER.value + ) assert ( RoleMapping.min_access_level(OperationType.UPDATE) == Role.CONTRIBUTOR.value @@ -90,10 +92,10 @@ def test_maximum_access_level(): def test_access_level_user(): """Test access_level property for USER role""" - prop = BaseAccessProperty(role=Role.USER) + prop = BaseAccessProperty(role=Role.MEMBER) expected = [OperationType.READ] assert prop.access_level == expected - assert prop.role.value == Role.USER.value + assert prop.role.value == Role.MEMBER.value def test_access_level_contributor(): @@ -128,10 +130,10 @@ def test_by_user_property(access_properties): assert "user3" in result assert result["user1"].role == Role.OWNER - assert result["user2"].role == Role.USER + assert result["user2"].role == Role.MEMBER assert result["user3"].role == Role.CONTRIBUTOR assert result["user1"].role.value == Role.OWNER.value - assert result["user2"].role.value == Role.USER.value + assert result["user2"].role.value == Role.MEMBER.value assert result["user3"].role.value == Role.CONTRIBUTOR.value @@ -145,9 +147,9 @@ def test_by_group_property(access_properties): assert "group2" in result assert result["group1"].role == Role.ADMIN - assert result["group2"].role == Role.USER + assert result["group2"].role == Role.MEMBER assert result["group1"].role.value == Role.ADMIN.value - assert result["group2"].role.value == Role.USER.value + assert result["group2"].role.value == Role.MEMBER.value @pytest.mark.usefixtures("access_properties") @@ -192,8 +194,8 @@ def test_operation_by_group_property(access_properties): def test_operation_by_user_multiple_same_operation(): """Test operation_by_user with multiple users having same operations""" user_access = [ - UserAccessProperty(user_id="user1", role=Role.USER), - UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty(user_id="user1", role=Role.MEMBER), + UserAccessProperty(user_id="user2", role=Role.MEMBER), UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), ] props = KItemAccessProperties(user_access=user_access) @@ -219,15 +221,15 @@ def test_operation_by_group_from_int(): assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} # Only user3 (CONTRIBUTOR) should have UPDATE access assert result[OperationType.UPDATE] == ["user3"] - assert props.by_user["user1"].role == Role.USER - assert props.by_user["user1"].role.value == Role.USER.value + assert props.by_user["user1"].role == Role.MEMBER + assert props.by_user["user1"].role.value == Role.MEMBER.value def test_operation_by_group_multiple_same_operation(): """Test operation_by_group with multiple groups having same operations""" group_access = [ - GroupAccessProperty(group_id="group1", role=Role.USER), - GroupAccessProperty(group_id="group2", role=Role.USER), + GroupAccessProperty(group_id="group1", role=Role.MEMBER), + GroupAccessProperty(group_id="group2", role=Role.MEMBER), GroupAccessProperty(group_id="group3", role=Role.ADMIN), ] props = KItemAccessProperties(group_access=group_access) @@ -238,7 +240,7 @@ def test_operation_by_group_multiple_same_operation(): # Only group3 (ADMIN) should have MANAGE access assert result[OperationType.MANAGE] == ["group3"] assert props.group_by_role[Role.ADMIN] == ["group3"] - assert props.group_by_role[Role.USER] == ["group1", "group2"] + assert props.group_by_role[Role.MEMBER] == ["group1", "group2"] def test_model_creation_with_defaults(): @@ -266,7 +268,7 @@ def test_duplicate_user_ids_raises_error(): """Test that duplicate user IDs raise ValueError""" user_access = [ UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user2", role=Role.USER), + UserAccessProperty(user_id="user2", role=Role.MEMBER), UserAccessProperty( user_id="user1", role=Role.CONTRIBUTOR ), # Duplicate @@ -288,7 +290,7 @@ def test_duplicate_group_ids_raises_error(): """Test that duplicate group IDs raise ValueError""" group_access = [ GroupAccessProperty(group_id="group1", role=Role.ADMIN), - GroupAccessProperty(group_id="group2", role=Role.USER), + GroupAccessProperty(group_id="group2", role=Role.MEMBER), GroupAccessProperty( group_id="group1", role=Role.CONTRIBUTOR ), # Duplicate @@ -309,11 +311,11 @@ def test_both_user_and_group_duplicates_raises_multiple_errors(): """Test that duplicates in both user and group access raise multiple errors""" user_access = [ UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user1", role=Role.USER), # Duplicate + UserAccessProperty(user_id="user1", role=Role.MEMBER), # Duplicate ] group_access = [ GroupAccessProperty(group_id="group1", role=Role.ADMIN), - GroupAccessProperty(group_id="group1", role=Role.USER), # Duplicate + GroupAccessProperty(group_id="group1", role=Role.MEMBER), # Duplicate ] with pytest.raises(ValidationError) as exc_info: @@ -344,7 +346,9 @@ def test_case_sensitive_ids(): """Test that IDs are case sensitive (no duplicates if different case)""" user_access = [ UserAccessProperty(user_id="User1", role=Role.OWNER), - UserAccessProperty(user_id="user1", role=Role.USER), # Different case + UserAccessProperty( + user_id="user1", role=Role.MEMBER + ), # Different case UserAccessProperty( user_id="USER1", role=Role.CONTRIBUTOR ), # Different case @@ -355,10 +359,10 @@ def test_case_sensitive_ids(): assert len(props.user_access) == 3 assert props.by_user["User1"].role == Role.OWNER - assert props.by_user["user1"].role == Role.USER + assert props.by_user["user1"].role == Role.MEMBER assert props.by_user["USER1"].role == Role.CONTRIBUTOR assert props.user_by_role[Role.OWNER] == ["User1"] - assert props.user_by_role[Role.USER] == ["user1"] + assert props.user_by_role[Role.MEMBER] == ["user1"] assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] @@ -375,8 +379,8 @@ def test_case_sensitive_ids_dict(): assert len(props.user_access) == 3 assert props.by_user["User1"].role == Role.OWNER - assert props.by_user["user1"].role == Role.USER + assert props.by_user["user1"].role == Role.MEMBER assert props.by_user["USER1"].role == Role.CONTRIBUTOR assert props.user_by_role[Role.OWNER] == ["User1"] - assert props.user_by_role[Role.USER] == ["user1"] + assert props.user_by_role[Role.MEMBER] == ["user1"] assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] diff --git a/tests/test_access_extended.py b/tests/test_access_extended.py index 5c4c19c..4ea34de 100644 --- a/tests/test_access_extended.py +++ b/tests/test_access_extended.py @@ -18,14 +18,14 @@ def test_role_ordering(): """Role integer values must be strictly ascending: USER < CONTRIBUTOR < OWNER < ADMIN.""" - assert Role.USER < Role.CONTRIBUTOR < Role.OWNER < Role.ADMIN + assert Role.MEMBER < Role.CONTRIBUTOR < Role.OWNER < Role.ADMIN def test_role_gte_comparison(): """>= on Role values must work correctly for threshold checks.""" assert Role.OWNER >= Role.CONTRIBUTOR assert Role.ADMIN >= Role.OWNER - assert not (Role.USER >= Role.CONTRIBUTOR) + assert not (Role.MEMBER >= Role.CONTRIBUTOR) # --------------------------------------------------------------------------- @@ -37,7 +37,7 @@ def test_min_access_level_returns_role_instance(): """min_access_level must return a Role member, not a plain int.""" result = RoleMapping.min_access_level(OperationType.READ) assert isinstance(result, Role) - assert result is Role.USER + assert result is Role.MEMBER def test_max_access_level_returns_role_instance(): @@ -50,7 +50,7 @@ def test_max_access_level_returns_role_instance(): @pytest.mark.parametrize( "operation, expected_min", [ - (OperationType.READ, Role.USER), + (OperationType.READ, Role.MEMBER), (OperationType.UPDATE, Role.CONTRIBUTOR), (OperationType.DELETE, Role.OWNER), (OperationType.MANAGE, Role.OWNER), @@ -132,13 +132,13 @@ def test_model_dump_json_produces_integer_roles(): """model_dump(mode='json') must produce integer role values for the wire format.""" props = KItemAccessProperties( user_access=[UserAccessProperty(user_id="u1", role=Role.OWNER)], - group_access=[GroupAccessProperty(group_id="g1", role=Role.USER)], + group_access=[GroupAccessProperty(group_id="g1", role=Role.MEMBER)], ) payload = props.model_dump(mode="json") assert payload["user_access"][0]["role"] == Role.OWNER.value assert isinstance(payload["user_access"][0]["role"], int) - assert payload["group_access"][0]["role"] == Role.USER.value + assert payload["group_access"][0]["role"] == Role.MEMBER.value assert isinstance(payload["group_access"][0]["role"], int) @@ -157,18 +157,18 @@ def test_round_trip_from_backend_dict(): backend_payload = { "user_access": [ {"user_id": "alice", "role": Role.OWNER.value}, - {"user_id": "bob", "role": Role.USER.value}, + {"user_id": "bob", "role": Role.MEMBER.value}, ], "group_access": [ - {"group_id": "dsms:internally-public", "role": Role.USER.value}, + {"group_id": "dsms:internally-public", "role": Role.MEMBER.value}, ], } props = KItemAccessProperties(**backend_payload) assert props.by_user["alice"].role is Role.OWNER - assert props.by_user["bob"].role is Role.USER - assert props.by_group["dsms:internally-public"].role is Role.USER + assert props.by_user["bob"].role is Role.MEMBER + assert props.by_group["dsms:internally-public"].role is Role.MEMBER # Serialise back and verify identity re_serialised = props.model_dump(mode="json") @@ -178,7 +178,7 @@ def test_round_trip_from_backend_dict(): } assert re_serialised["group_access"][0] == { "group_id": "dsms:internally-public", - "role": Role.USER.value, + "role": Role.MEMBER.value, } @@ -192,13 +192,13 @@ def test_user_by_role(): props = KItemAccessProperties( user_access=[ UserAccessProperty(user_id="alice", role=Role.OWNER), - UserAccessProperty(user_id="bob", role=Role.USER), - UserAccessProperty(user_id="carol", role=Role.USER), + UserAccessProperty(user_id="bob", role=Role.MEMBER), + UserAccessProperty(user_id="carol", role=Role.MEMBER), ] ) by_role = props.user_by_role assert by_role[Role.OWNER] == ["alice"] - assert set(by_role[Role.USER]) == {"bob", "carol"} + assert set(by_role[Role.MEMBER]) == {"bob", "carol"} assert Role.CONTRIBUTOR not in by_role From 13ff04600e76ce625860d721784cffef980348e1 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 12:03:12 -0400 Subject: [PATCH 34/48] Add firstName, lastName, email to User model The user-service GET /api/users/{id} endpoint returns these fields. Declaring them as Optional allows SDK consumers to access full name information without falling back to raw dicts. --- dsms/knowledge/groups/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 7afae34..642c47f 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -13,6 +13,13 @@ class User(BaseModel): id: str = Field(..., description="The unique identifier of the user.") username: str = Field(..., description="The username of the user.") + firstName: Optional[str] = Field( + None, description="First name of the user." + ) + lastName: Optional[str] = Field(None, description="Last name of the user.") + email: Optional[str] = Field( + None, description="Email address of the user." + ) user_groups: Optional[List["BaseGroup"]] = Field( None, description="A list of groups the user belongs to." ) From 492ebdbbba14e6f171e7c8c316258601377a5d1d Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Fri, 5 Jun 2026 12:05:09 -0400 Subject: [PATCH 35/48] Update documentation, notebooks, and tooling for v5.0.0 - Add CHANGELOG.md and CONTRIBUTING.md - Update README: capabilities list, compatibility table, copyright year, replace Authors section with Contributing pointer - Update docs/dsms_sdk/dsms_sdk.md and dsms_kitem_schema.md: new KItem fields (contexts, access_properties, schema_data), Widget table, KItemCompactedModel, KItemAccessProperties, KItemSchemaData - Add docs/release-checklist.md and link it from docs/index.md - Add scripts/run_notebooks.sh for notebook test and refresh modes - Add pytest-nbmake to [tests] extras in setup.cfg - Rename 3_updation.ipynb to 3_updating.ipynb; update all 8 notebooks: fix typos, add v5 API examples (access_properties, KTypeV2, contexts, context SPARQL), make self-contained (no hardcoded UUIDs), wrap asynchronous operations (subgraph, dataframe, app run) in try/except, replace non-existent Testingmachine ktype with MeasurementDevice, use unique sdk-tutorial ktype ID in nb7 to avoid name collision, fix Specimen Number widget validation (scalar not list) --- CHANGELOG.md | 114 +++ CONTRIBUTING.md | 151 ++++ README.md | 40 +- docs/dsms_sdk/dsms_kitem_schema.md | 166 +++- docs/dsms_sdk/dsms_sdk.md | 23 +- docs/dsms_sdk/tutorials/1_introduction.ipynb | 23 +- docs/dsms_sdk/tutorials/2_creation.ipynb | 422 ++------- docs/dsms_sdk/tutorials/3_updating.ipynb | 235 +++++ docs/dsms_sdk/tutorials/3_updation.ipynb | 360 -------- docs/dsms_sdk/tutorials/4_deletion.ipynb | 148 +-- docs/dsms_sdk/tutorials/5_search.ipynb | 839 ++---------------- docs/dsms_sdk/tutorials/6_apps.ipynb | 139 +-- docs/dsms_sdk/tutorials/7_ktypes.ipynb | 663 +++----------- .../dsms_sdk/tutorials/8_kitem_contexts.ipynb | 272 +++--- docs/index.md | 1 + docs/release-checklist.md | 215 +++++ scripts/run_notebooks.sh | 77 ++ setup.cfg | 1 + 18 files changed, 1548 insertions(+), 2341 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/dsms_sdk/tutorials/3_updating.ipynb delete mode 100644 docs/dsms_sdk/tutorials/3_updation.ipynb create mode 100644 docs/release-checklist.md create mode 100755 scripts/run_notebooks.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9b01bf7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,114 @@ +# Changelog + +All notable changes to this project are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [5.0.0] — unreleased + +### Added + +**User model** +- `User` gains optional `firstName`, `lastName`, and `email` fields populated from the Keycloak user profile. + +**Access control (RBAC)** +- `KItemAccessProperties` model with `user_access` and `group_access` lists for per-KItem role assignments. +- `Role` enum (`MEMBER=1`, `CONTRIBUTOR=2`, `OWNER=3`, `ADMIN=4`) and `OperationType` enum (`create`, `read`, `update`, `delete`, `manage`). +- `RoleMapping` enum with `get_operations`, `min_access_level`, and `max_access_level` helpers. +- `UserAccessProperty` and `GroupAccessProperty` sub-models. +- `DSMS.user_groups` and `DSMS.users` cached properties with `refresh_user_groups()` / `refresh_users()` invalidation. +- `DSMS.get_user(user_id)` convenience method. +- `Group`, `User`, `GroupList`, `UserList` models (`dsms.knowledge.groups`). +- `INTERNALLY_PUBLIC_GROUP` / `EXTERNALLY_PUBLIC_GROUP` constants, configurable via environment variables. +- `refresh_public_groups(config)` to avoid import-time staleness when custom group IDs are used. + +**KType v2 semantic-spec subsystem** +- `KTypeSpec` model capturing the full `ktype_spec` database record: ontology classes, relations, semantic schema references, inheritance, tags, versioning, stash state. +- Sub-models: `OntologyClassSpec`, `SemanticSchemaRef`, `RelationSpec`. +- `KTypeV2(KType)` response model with optional `spec: KTypeSpec` field. +- Request models: `CreateKTypeRequest`, `ImportFromUrlRequest`, `KTypeSpecPayload`. +- Remote-repository models: `RemoteKTypeSummary`, `RemoteKTypeVersion`, `RemoteSchemaInfo`, `RemoteSchemaVersionInfo`, `SpecDiffField`, `RemoteDiffOut`. +- Full v2 CRUD surface on `DSMS`: `get_v2_ktypes`, `get_v2_ktype`, `create_v2_ktype`, `import_v2_ktype`, `update_v2_ktype`, `delete_v2_ktype`, `restore_v2_ktype_stash`, `refresh_v2_ktype`, `export_v2_ktype`, `list_remote_v2_ktypes`, `list_remote_schemas`, `list_remote_ktype_versions`, `get_v2_ktype_remote_diff`. +- `DSMS.get_ktypes_by_parent(parent_id)` for the `?parent=` filter on the v1 list endpoint. + +**Schema data on KItems** +- `KItemSchemaData` model (`schema_id`, `content`) and `KItemSchemaDataList` helper with `.by_schema_id` lookup. +- `KItem.schema_data: Optional[List[KItemSchemaData]]` field. +- Commit flow syncs `schema_data` changes via `PUT`/`DELETE` on `/api/knowledge/{kitem_id}/schema-data/{schema_id}`. + +**Context SPARQL** +- `SparqlInterface.query_context(context_id, query)` — `POST /api/knowledge/sparql/context`. +- `SparqlInterface.graph_context(context_id, query)` — `POST /api/knowledge/graph/context`. + +**Search and list additions** +- `DSMS.search()` gains `contexts: List[str]` (filter by context KItem IDs) and `attachment_extensions: List[str]` (filter by file extension). +- `DSMS.get_kitems()` gains `name: str` for substring filtering. + +**KItemCompactedModel additions** +- `has_contexts: bool` — whether the KItem belongs to at least one context. +- `attachment_extensions: Optional[List[str]]` — unique file extensions in the KItem's attachments. +- `avatar_exists` moved from `KItem` to the shared `KItemCompactedModel` base. + +### Changed + +- `KItem.contexts` field now properly tracks changes in `_get_kitems_diffs()`. +- `KItem.access_properties` changes are tracked and committed. +- `get_user_by_id` now accepts `dsms` as its first argument (consistent with all util functions) and returns a typed `User` object. +- `Role.min_access_level` / `max_access_level` now return `Role` objects and raise `ValueError` for operations not granted by any role (e.g. `CREATE`). + +### Deprecated + +- `KItem.authors` — the server no longer populates this field. Use `access_properties` instead. +- `KItem.rdf_exists` — the server no longer populates this field. +- `KItem.user_groups` — legacy field still supported but superseded by `access_properties`. + +### Maintenance + +- Upgraded pre-commit hooks: `pre-commit-hooks` v4→v6, `black` 23→26, `isort` 5→8, `setup-cfg-fmt` v2→v3, `bandit` 1.9.3→1.9.4. +- Aligned `isort` line length to match `black`'s 79-character limit. +- Relaxed dependency pins: `rdflib>=6,<8`, `pandas>=2,<4`, `segno>=1.6,<2`, `pydantic-settings>=2,<3`, `oyaml>=1`. +- Dropped Python 3.8 and 3.9 support. + +--- + +## [4.0.0] + +### Added + +- Pydantic v2 migration (`BaseModel`, `field_validator`, `field_serializer`, `model_dump`). +- Service-account authentication via `client_id` / `client_secret` (Keycloak). +- `KItemAccessProperties` groundwork, `UserGroup` model. +- `AppConfig` and `DSMS.apps` for managing application configurations. +- `ProcessSchema` and `WebformSchema` models on `KType`. + +### Changed + +- Minimum Python version raised to 3.10. +- `KType.id` accepts both `UUID` and `str`. +- `Configuration` uses `pydantic-settings` for environment-variable loading. + +--- + +## [3.x] + +- Triplestore / SPARQL interface (`SparqlInterface`, subgraph CRUD). +- `Attachment`, `Avatar`, `ExternalLink`, `LinkedKItems` property models. +- DataFrame integration via `dataframe` field on `KItem`. +- `DSMS.search()` initial implementation. + +--- + +## [2.x] + +- Initial Pydantic v1 models for `KItem` and `KType`. +- Basic CRUD operations via `DSMS.add()`, `DSMS.delete()`, `DSMS.commit()`. +- Annotation support. + +--- + +## [< 2.0.0] + +- Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4b509ea --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,151 @@ +# Contributing to DSMS Python SDK + +Thank you for considering a contribution! This document explains how to set up a development environment, run the checks we require, and submit changes. + +--- + +## Table of Contents + +1. [Development setup](#development-setup) +2. [Code style and linting](#code-style-and-linting) +3. [Pre-commit hooks](#pre-commit-hooks) +4. [Testing](#testing) +5. [Branching and commits](#branching-and-commits) +6. [Opening a pull request](#opening-a-pull-request) +7. [Versioning](#versioning) +8. [Reporting issues](#reporting-issues) + +--- + +## Development setup + +```bash +git clone git@github.com:MI-FraunhoferIWM/dsms-python-sdk.git +cd dsms-python-sdk +python -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +``` + +The `[dev]` extra installs all development dependencies including linters, test runners, and the pre-commit framework. + +--- + +## Code style and linting + +We enforce a consistent style automatically via pre-commit hooks (see below). The key rules are: + +| Tool | Configuration | +|:---------:|:---------------------------------------------:| +| `black` | Line length 79, enforced on all `.py` files | +| `isort` | Profile `black`, line length 79 | +| `flake8` | Default rules, line length inferred from black | +| `pylint` | `fail-under=10.0`; see `.pylintrc` for disabled checks | +| `bandit` | Security linting | +| `pyupgrade` | Enforces modern Python syntax | + +Do **not** bypass hooks with `--no-verify`. If a hook fails, fix the underlying issue. + +--- + +## Pre-commit hooks + +Install the hooks once after cloning: + +```bash +pip install pre-commit +pre-commit install +``` + +Run manually against all changed files: + +```bash +pre-commit run --files ... +``` + +Run against all files in the repo: + +```bash +pre-commit run --all-files +``` + +To update hook versions to the latest stable releases: + +```bash +pre-commit autoupdate +``` + +--- + +## Testing + +Run the unit test suite with: + +```bash +pytest +``` + +Tests live under `tests/`. We do not mock the database in integration tests — if you add a test that touches the backend, it must run against a real DSMS instance configured via environment variables (see `Configuration` in `dsms/core/configuration.py`). + +**Tutorial notebooks** can be tested against a live instance with: + +```bash +./scripts/run_notebooks.sh +``` + +To re-execute notebooks and save outputs in-place (for documentation commits): + +```bash +./scripts/run_notebooks.sh --refresh +``` + +See `scripts/run_notebooks.sh --help` (or read the script header) for full usage. Requires `pip install -e ".[docs,tests]"` and a reachable DSMS instance. + +--- + +## Branching and commits + +- Base feature branches off `main`. +- Use descriptive branch names, e.g. `feature/ktype-v2-subsystem` or `fix/search-context-filter`. +- Keep commits focused. One logical change per commit. +- Write commit messages in the imperative mood: *"Add schema_data field to KItem"*, not *"Added"* or *"Adding"*. +- Do not amend published commits. + +--- + +## Opening a pull request + +1. Push your branch and open a PR against `main`. +2. Fill in the PR template — at minimum, describe **what** changed and **why**. +3. Ensure all CI checks pass before requesting a review. +4. Add an entry to `CHANGELOG.md` under the relevant unreleased section. +5. Update any affected documentation in `docs/`. + +PRs that introduce new public API surface should update: +- `dsms/knowledge/__init__.py` — re-export new models. +- `docs/dsms_sdk/dsms_kitem_schema.md` or `docs/dsms_sdk/dsms_sdk.md` — document new fields/methods. + +--- + +## Versioning + +This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): + +- **MAJOR** — breaking API changes. +- **MINOR** — backward-compatible new functionality. +- **PATCH** — backward-compatible bug fixes. + +The version is set in `setup.cfg` (`version = vMAJOR.MINOR.PATCH`). Update it and `CHANGELOG.md` together as part of a release PR. + +The SDK version must stay compatible with the target DSMS backend version. See the compatibility table in `README.md`. + +--- + +## Reporting issues + +Please open an issue at and include: + +- SDK version (`pip show dsms-sdk`). +- Python version. +- A minimal reproducible example. +- The full traceback if applicable. diff --git a/README.md b/README.md index dc34313..edbd983 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,18 @@ pip install -e . The SDK provides a general Python interface to a remote DSMS deployment, allowing users to access, store and link data in a DSMS instance easily and safely. The package provides the following main capabilities: -- Managing Knowledge-Items (KItems), which are data instances of an explicitly defined semantic class type (KType) - - Creating, updating and deleting meta data and properties, e.g. date, operator, material response data for a conducted tensile test - - Administrating authorship, contact information and supplementary information upon making changes or adding KItems - - Semantic annotation of KItems -- Conduct simple free-text searches within the DSMS instance including filters (e.g. limiting the search for certain materials) as well as a more experts-aware SPARQL interface -- Linking KItems to other KItems -- Linking Apps to KItems, triggererd, for example, during a file upload +- Managing Knowledge Items (KItems), which are data instances of an explicitly defined semantic class type (KType) + - Creating, updating and deleting metadata and properties, e.g. date, operator, material response data for a conducted tensile test + - Contact information and supplementary information upon making changes or adding KItems + - Semantic annotation of KItems + - Attaching semantic schema data (ontology-class instance data) to KItems +- Managing Knowledge Types (KTypes), including the v2 semantic-spec subsystem for defining ontology classes, relations, and schema references +- Role-based access control (RBAC) per KItem: assign users and groups to `MEMBER`, `CONTRIBUTOR`, `OWNER`, or `ADMIN` roles +- Conduct free-text searches within the DSMS instance with filters (KType, annotation, context membership, attachment extension) as well as a full SPARQL interface (including context-scoped queries) +- Linking KItems to other KItems, and grouping them via context KItems +- Linking Apps to KItems, triggered, for example, during a file upload - Performing simple file upload and download using attachments to KItems -- Export of a knowledge (sub) graph as common serializations (.ttl, .json) +- Export of a knowledge (sub)graph as common serializations (.ttl, .json) ## Documentation @@ -49,7 +52,8 @@ Please take the compability of the SDK version with the DSMS version into accoun | >=3.0.4, <3.1.0 | >=3.0.5, <3.1.0 | | >=3.1.0, <3.2.2 | >=3.1.0, <3.2.1 | | >=3.2.2 | >=3.2.1, <4.0.0 | -| >=4.0.0 | >=4.0.0 | +| >=4.0.0, <5.0.0 | >=4.0.0, <5.0.0 | +| >=5.0.0 | >=5.0.0 | ## Tutorials @@ -57,7 +61,7 @@ Please take the compability of the SDK version with the DSMS version into accoun Please have a look at our tutorials on _readthedocs_: * [1. Introduction](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/1_introduction.html) * [2. Creation](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/2_creation.html) -* [3. Updation](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/3_updation.html) +* [3. Updating](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/3_updating.html) * [4. Deletion](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/4_deletion.html) * [5. Search](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/5_search.html) * [6. Apps](https://dsms-python-sdk.readthedocs.io/en/latest/dsms_sdk/tutorials/6_apps.html) @@ -65,23 +69,15 @@ Please have a look at our tutorials on _readthedocs_: Or try our Jupyter Notebooks: * [1. Introduction](docs/dsms_sdk/tutorials/1_introduction.ipynb) * [2. Creation](docs/dsms_sdk/tutorials/2_creation.ipynb) -* [3. Updation](docs/dsms_sdk/tutorials/3_updation.ipynb) +* [3. Updating](docs/dsms_sdk/tutorials/3_updating.ipynb) * [4. Deletion](docs/dsms_sdk/tutorials/4_deletion.ipynb) * [5. Search](docs/dsms_sdk/tutorials/5_search.ipynb) * [6. Apps](docs/dsms_sdk/tutorials/6_apps.ipynb) * [7. KTypes](docs/dsms_sdk/tutorials/7_ktypes.ipynb) -## Authors +## Contributing -[Matthias Büschelberger](mailto:matthias.bueschelberger@iwm.fraunhofer.de) (Fraunhofer Institute for Mechanics of Materials IWM) - -[Yoav Nahshon](mailto:yoav.nahshon@iwm.fraunhofer.de) (Fraunhofer Institute for Mechanics of Materials IWM) - -[Pablo De Andres](mailto:pablo.de.andres@iwm.fraunhofer.de) (Fraunhofer Institute for Mechanics of Materials IWM) - -[Priyabrat Mishra](mailto:priyabrat.mishra@iwm.fraunhofer.de) (Fraunhofer Institute for Mechanics of Materials IWM) - -[Arjun Gopalakrishnan](mailto:arjun.gopalakrishnan@iwm.fraunhofer.de) (Fraunhofer Institute for Mechanics of Materials IWM) +See [CONTRIBUTING.md](CONTRIBUTING.md). All contributors are listed on the [GitHub contributors page](https://github.com/MI-FraunhoferIWM/dsms-python-sdk/graphs/contributors). ## License @@ -90,6 +86,6 @@ This project is licensed under the BSD 3-Clause. See the LICENSE file for more i ## Disclaimer -Copyright (c) 2014-2024, Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. acting on behalf of its Fraunhofer IWM. +Copyright (c) 2014-2026, Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. acting on behalf of its Fraunhofer IWM. Contact: [Matthias Büschelberger](mailto:matthias.bueschelberger@iwm.fraunhofer.de) diff --git a/docs/dsms_sdk/dsms_kitem_schema.md b/docs/dsms_sdk/dsms_kitem_schema.md index 4ad3854..a2b7c0a 100644 --- a/docs/dsms_sdk/dsms_kitem_schema.md +++ b/docs/dsms_sdk/dsms_kitem_schema.md @@ -9,6 +9,8 @@ The schema contains complex types and references, indicating an advanced usage s ![kitem_schema_uml](../assets/images/UML_KItem_schema.jpg) +`KItem` inherits the compacted base fields listed in [KItemCompactedModel fields](#kitemcompactedmodel-fields) below, and adds the following full-detail fields: + | Field Name | Description | Type | Default | Property Namespace | Required / Optional | |:-----------------:|:--------------------------------------------------------------------------------------------------------:|:-------------------------------------------------:|:--------:|:------------------:|:-----------------:| | Name | Human-readable name of the KItem. | string | Not Applicable | `name` | Required | @@ -18,7 +20,6 @@ The schema contains complex types and references, indicating an advanced usage s | Created At | Timestamp of when the KItem was created. | Union[string, datetime] | `None` | `created_at` | Automatically generated | | Updated At | Timestamp of when the KItem was updated. | Union[string, datetime] | `None` | `updated_at` | Automatically generated | | Avatar | The avatar of the KItem. | Union[[Avatar](#avatar-fields), Dict[str, Any]] | `None` | `avatar` | Optional | -| Avatar Exists | Whether the KItem holds an avatar or not. | boolean | `False` | `avatar_exists` | Automatically generated | | [KItemCustomPropertiesModel](#kitemcustompropertiesmodel) | A set of custom properties related to the KItem. | Any | `None` | `custom_properties`| Optional | | Summary | A brief human-readable summary of the KItem | string | `None` | `summary` | Optional | | Apps | A list of applications associated with the KItem | List[[App](#app-fields)] | `[ ]` | `apps` | Optional | @@ -26,14 +27,32 @@ The schema contains complex types and references, indicating an advanced usage s | Affiliations | A list of affiliations associated with the KItem | List[[Affiliation](#affiliation-fields)] | `[ ]` | `affiliations` | Optional | | Contacts | Contact information related to the KItem | List[[ContactInfo](#contactinfo-fields)] | `[ ]` | `contacts` | Optional | | External Links | A list of external links related to the KItem | List[[ExternalLink](#externallink-fields)] | `[ ]` | `external_links` | Optional | -| Attachments | A list of file attachments associated with the KItem | List [Union [[Attachment](#attachment-fields)], string] | `[ ]` | `attachments` | Optional | +| Attachments | A list of file attachments associated with the KItem | List[Union[[Attachment](#attachment-fields), string]] | `[ ]` | `attachments` | Optional | | Dataframe | Dataframe associated with the KItem, e.g. a time series | Union[List[[Column](#column-fields)], pd.DataFrame, Dictionary[string, Union[List, Dictionary]]] | `None` | `dataframe` | Optional | | Linked KItems | List of other KItems linked to this KItem | List[Union[[LinkedKItem](#linkedkitem-fields), "KItem"]] | `[ ]` | `linked_kitems` | Optional | -| User Groups | User groups with access to this KItem | List[[UserGroup](#usergroup-fields)] | `[ ]` | `user_groups` | Optional | +| Contexts | Context KItems this KItem belongs to | List[Union[KItem, KItemCompactedModel]] | `[ ]` | `contexts` | Optional | +| Access Properties | Role-based access control entries for users and groups | [KItemAccessProperties](#kitemaccesrproperties-fields) | `None` | `access_properties`| Optional | +| Schema Data | Semantic schema data entries (ontology-class instance data) associated with this KItem | List[[KItemSchemaData](#kitemschemeadata-fields)] | `None` | `schema_data` | Optional | +| User Groups | *(Legacy)* User groups with access to this KItem. Prefer `access_properties` for new code. | List[[UserGroup](#usergroup-fields)] | `[ ]` | `user_groups` | Optional | +| Authors | *(Deprecated)* Authorship list. No longer populated by the server; use `access_properties` instead. | List[Author] | `[ ]` | `authors` | Deprecated | +| RDF Exists | *(Deprecated)* Whether the KItem holds an RDF graph. No longer populated by the server. | boolean | `False` | `rdf_exists` | Deprecated | + +## KItemCompactedModel Fields + +`KItemCompactedModel` is the lightweight representation returned by the search endpoint. `KItem` extends it with all the full-detail fields listed above. + +| Field Name | Description | Type | Default | Property Namespace | Required / Optional | +|:----------------------:|:------------------------------------------------------------------------:|:---------------------:|:-------:|:------------------------:|:----------------------------:| +| Name | Human-readable name of the KItem. | string | — | `name` | Required | +| ID | ID of the KItem. | UUID | auto | `id` | Optional | +| Ktype ID | The type ID of the KItem. | Union[Enum, string] | — | `ktype_id` | Required | +| Slug | A unique slug identifier, minimum 4 characters. | string | `None` | `slug` | Optional | +| Avatar Exists | Whether the KItem holds an avatar or not. | boolean | `False` | `avatar_exists` | Automatically generated | +| Has Contexts | Whether the KItem belongs to at least one context. | boolean | `False` | `has_contexts` | Automatically generated | +| Attachment Extensions | Unique file extensions present in this KItem's attachments. | List[string] | `None` | `attachment_extensions` | Automatically generated | ### Example Usage ```python - item = KItem( name="Glass Bending machine 01", slug="1234", @@ -41,25 +60,25 @@ item = KItem( custom_properties={"location": "Room01", "max_force": "100Pa"}, summary="This is a summary", apps=[ - {"executable": "my_analysis_file", - "title": "Analysis", - "description": "Analysis the tensile strength from machine data"} + { + "executable": "my_analysis_file", + "title": "Analysis", + "description": "Analyse the tensile strength from machine data", + } ], annotations=["http://example.org/sample_kitem/annotation"], - affiliations=[ - {"name": "Institute ABC"} - ], - contacts=[ - {"name": "John Doe", "email": "john.doe@example.com"} - ], - external_links=[ - {"label": "Project Website", "url": "https://example.com"} - ], + affiliations=[{"name": "Institute ABC"}], + contacts=[{"name": "John Doe", "email": "john.doe@example.com"}], + external_links=[{"label": "Project Website", "url": "https://example.com"}], attachments=["research_data.csv"], linked_kitems=[another_kitem], - user_groups=[ - {"group_id": "33305", "name": "DigiMaterials"} - ] + access_properties={ + "user_access": [{"user_id": "abc-123", "role": 3}], + "group_access": [{"group_id": "g-456", "role": 1}], + }, + schema_data=[ + {"schema_id": "https://example.org/ontology/MyClass", "content": {"key": "value"}} + ], ) ``` @@ -271,6 +290,83 @@ sample_kitem.user_groups = [ ] ``` +## KItemAccessProperties Fields + +`KItemAccessProperties` controls who can read, update, delete and manage a KItem. It contains two lists: one for individual users and one for groups. + +### Role Values + +| Role name | Integer value | Permitted operations | +|:-------------:|:-------------:|:---------------------------------------------:| +| `MEMBER` | 1 | READ | +| `CONTRIBUTOR` | 2 | READ, UPDATE | +| `OWNER` | 3 | READ, UPDATE, DELETE, MANAGE | +| `ADMIN` | 4 | READ, UPDATE, DELETE, MANAGE | + +### KItemAccessProperties Sub-fields + +| Field Name | Description | Type | Default | Property Namespace | Required/Optional | +|:-------------:|:----------------------------------------:|:--------------------------------:|:-------:|:------------------:|:-----------------:| +| User Access | Per-user role assignments | List[[UserAccessProperty](#useraccessproperty-fields)] | `[]` | `user_access` | Optional | +| Group Access | Per-group role assignments | List[[GroupAccessProperty](#groupaccessproperty-fields)] | `[]` | `group_access` | Optional | + +### UserAccessProperty Fields + +| Field Name | Description | Type | Default | Property Namespace | Required/Optional | +|:----------:|:-----------------------:|:------:|:--------------:|:------------------:|:-----------------:| +| User ID | UUID of the user | string | Not Applicable | `user_id` | Required | +| Role | Role assigned to user | int (1–4) or Role name | Not Applicable | `role` | Required | + +### GroupAccessProperty Fields + +| Field Name | Description | Type | Default | Property Namespace | Required/Optional | +|:----------:|:-----------------------:|:------:|:--------------:|:------------------:|:-----------------:| +| Group ID | UUID of the group | string | Not Applicable | `group_id` | Required | +| Role | Role assigned to group | int (1–4) or Role name | Not Applicable | `role` | Required | + +### Example Usage +```python +from dsms.knowledge.properties.access import KItemAccessProperties, Role + +# Assign a user as OWNER and a group as MEMBER +item.access_properties = KItemAccessProperties( + user_access=[{"user_id": "abc-123", "role": Role.OWNER}], + group_access=[{"group_id": "g-456", "role": Role.MEMBER}], +) + +# Query which users can perform a given operation +from dsms.knowledge.properties.access import OperationType +print(item.access_properties.operation_by_user[OperationType.UPDATE]) + +# Look up the minimum role required to delete +from dsms.knowledge.properties.access import RoleMapping +print(RoleMapping.min_access_level(OperationType.DELETE)) # Role.OWNER +``` + +## KItemSchemaData Fields + +`KItemSchemaData` stores a single semantic schema instance attached to a KItem. Each entry maps an ontology-class IRI (the `schema_id`) to a free-form content dictionary. + +| Field Name | Description | Type | Default | Property Namespace | Required/Optional | +|:----------:|:-----------------------------------------------:|:---------------------:|:--------------:|:------------------:|:-----------------:| +| Schema ID | Ontology class IRI that identifies the schema | string | Not Applicable | `schema_id` | Required | +| Content | Free-form instance data for that schema | Dict[string, Any] | `None` | `content` | Optional | + +### Example Usage +```python +item.schema_data = [ + { + "schema_id": "https://example.org/ontology/TensileTest", + "content": {"strain_rate": 0.001, "temperature_K": 293}, + } +] + +# Access by schema ID +from dsms.knowledge.properties.schema_data import KItemSchemaDataList +entries = KItemSchemaDataList(item.schema_data) +test_entry = entries.by_schema_id["https://example.org/ontology/TensileTest"] +``` + ## KItemCustomPropertiesModel | Sub-Property Name | Description | Type | Default | Property Namespace | Required/Optional | @@ -328,15 +424,23 @@ sample_kitem.user_groups = [ ## Widget Fields -| Value | Description | -|:---------------:|:--------------------------------:| -| `Text` | Text input widget | -| `File` | File input widget | -| `Textarea` | Multiline text input widget | -| `Number` | Numeric input widget | -| `Slider` | Slider input widget | -| `Checkbox` | Checkbox input widget | -| `Select` | Dropdown select widget | -| `Radio` | Radio button widget | -| `Knowledge item`| Knowledge item selector widget | -| `Multi-select` | Multi-select widget | +| Value | Description | +|:------------------:|:--------------------------------------------:| +| `Array group` | Repeating group of fields | +| `Checkbox` | Boolean checkbox widget | +| `Date` | Date picker widget | +| `Date-time` | Date and time picker widget | +| `File` | File upload widget | +| `Key-value pairs` | Free-form key/value map widget | +| `Knowledge item` | Knowledge item selector widget | +| `LaTeX` | LaTeX-rendered text widget | +| `Multi-select` | Multi-select dropdown widget | +| `Number` | Numeric input widget | +| `Radio` | Radio button widget | +| `Select` | Dropdown select widget | +| `Slider` | Slider input widget | +| `Star rating` | Star-rating widget | +| `Text` | Single-line text input widget | +| `Textarea` | Multi-line text input widget | +| `URL` | URL input widget | +| `Vocabulary select`| Controlled-vocabulary term selector widget | diff --git a/docs/dsms_sdk/dsms_sdk.md b/docs/dsms_sdk/dsms_sdk.md index 2bb1425..77ec641 100644 --- a/docs/dsms_sdk/dsms_sdk.md +++ b/docs/dsms_sdk/dsms_sdk.md @@ -18,15 +18,17 @@ pip install dsms-sdk ... and start connecting to your central DSMS instance remotely, e.g. by integrating it into your own Python scripts and packages The SDK functionalities are listed below: -1. Managing Knowledge-Items. -2. Creating, updating and deleting meta data and properties, e.g. date, operator, material response data for a conducted tensile test. -3. Administrating authorship, contact information and supplementary information. -4. Semantic annotation of K-Items. -5. Conduct simple free-text searches and SPARQL queries. -6. Linking K-Items to other K-Items. -7. Linking Apps to K-Items, triggered, for example, during a file upload. -8. Performing simple file upload and download of file attachments. -9. Export of a knowledge (sub) graph into TTL/JSON-LD. +1. Managing Knowledge Items (KItems) — create, update and delete metadata and properties. +2. Contact information and supplementary information for KItems. +3. Semantic annotation of KItems. +4. Attaching semantic schema data (ontology-class instance data) to KItems. +5. Managing Knowledge Types (KTypes), including the v2 semantic-spec subsystem. +6. Role-based access control (RBAC) per KItem: assign users and groups to roles (`MEMBER`, `CONTRIBUTOR`, `OWNER`, `ADMIN`). +7. Free-text search with filters (KType, annotation, context membership, attachment extension) and a full SPARQL interface including context-scoped queries. +8. Linking KItems to other KItems, and grouping them via context KItems. +9. Linking Apps to KItems, triggered, for example, during a file upload. +10. Performing simple file upload and download of file attachments. +11. Export of a knowledge (sub)graph into TTL/JSON-LD. Click on the link to go to the Github repository of the Python based DSMS-SDK : [Git repo](https://github.com/MI-FraunhoferIWM/dsms-python-sdk) @@ -60,7 +62,8 @@ Please take the compability of the SDK version with the DSMS version into accoun | >=3.0.4, <3.1.0 | >=3.0.5, <3.1.0 | | >=3.1.0, <3.2.2 | >=3.1.0, <3.2.1 | | >=3.2.2 | >=3.2.1, <4.0.0 | -| >=4.0.0 | >=4.0.0 | +| >=4.0.0, <5.0.0 | >=4.0.0, <5.0.0 | +| >=5.0.0 | >=5.0.0 | #### Method 1: Via PyPI diff --git a/docs/dsms_sdk/tutorials/1_introduction.ipynb b/docs/dsms_sdk/tutorials/1_introduction.ipynb index d8ddc2e..bb26faa 100644 --- a/docs/dsms_sdk/tutorials/1_introduction.ipynb +++ b/docs/dsms_sdk/tutorials/1_introduction.ipynb @@ -14,7 +14,7 @@ "metadata": {}, "source": [ "### 1.1. Setting up\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", "\n", "Now let us import the needed classes and functions for this tutorial." ] @@ -41,7 +41,8 @@ "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { @@ -546,6 +547,24 @@ "for ktype in dsms.ktypes:\n", " print(ktype)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Note on deprecated fields\n", + "\n", + "Some fields visible in KItem outputs above (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. Use `access_properties` (a `KItemAccessProperties` object with `user_access` and `group_access` lists) for access control. These deprecated fields remain in the model for backward compatibility only.\n", + "\n", + "Upcoming tutorials cover `access_properties`, `schema_data` (semantic schema instance data), and `contexts` (grouping KItems into context containers)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You have completed the introduction tutorial. Continue with [2. Creation](2_creation.ipynb) to learn how to create new KItems." + ] } ], "metadata": { diff --git a/docs/dsms_sdk/tutorials/2_creation.ipynb b/docs/dsms_sdk/tutorials/2_creation.ipynb index b7ac841..b672eda 100644 --- a/docs/dsms_sdk/tutorials/2_creation.ipynb +++ b/docs/dsms_sdk/tutorials/2_creation.ipynb @@ -6,7 +6,7 @@ "source": [ "# 2. Create KItems with the SDK\n", "\n", - "In this tutorial we see how to create new Kitems." + "In this tutorial we see how to create new KItems." ] }, { @@ -14,14 +14,14 @@ "metadata": {}, "source": [ "### 2.1. Setting up\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", "\n", "Now let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -37,11 +37,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { @@ -49,110 +50,37 @@ "metadata": {}, "source": [ "\n", - "### 2.2: Create KItems" + "### 2.2. Create KItems" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can make new KItems by simple class-initiation: (Make sure existing KItems are not given as input). \n", - "#" + "We can make new KItems by simple class-initiation. (Do not pass an existing KItem as input, as this will raise an error.)" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:406: UserWarning: A flat dictionary was provided for custom properties.\n", - " Will be transformed into `KItemCustomPropertiesModel`.\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "kitem:\n", - " name: Specimen123\n", - " ktype_id: specimen\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 0.5\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item = KItem(\n", - " name=\"Specimen123\",\n", - " ktype_id=dsms.ktypes.Specimen,\n", - " custom_properties = {\n", - " \"Width\": 0.5,\n", - " \"Length\": [0.1, 0.2],\n", - " }\n", - ")\n", - "\n", - "item" + "item = KItem(\n name=\"Specimen123\",\n ktype_id=dsms.ktypes.Specimen,\n custom_properties = {\n \"Width\": 0.5,\n \"Length\": 0.15,\n }\n)\n\nitem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Remember: changes are only syncronized with the DSMS when you call the `commit`-method:" + "Remember: changes are only synchronized with the DSMS when you call the `commit`-method:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'https://bue.materials-data.space/knowledge/specimen/specimen123-5cb3a95e'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dsms.add(item)\n", "dsms.commit()\n", @@ -168,70 +96,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 5cb3a95e-4aac-434d-81ba-7c242fd245aa\n", - " name: Specimen123\n", - " ktype_id: specimen\n", - " slug: specimen123-5cb3a95e\n", - " annotations: []\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:29:01.826691\n", - " updated_at: 2025-08-13 14:29:01.826691\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 0.5\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item" ] @@ -245,20 +112,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Specimen123'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.name" ] @@ -267,25 +123,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As well as the id of the kitem we can do it as follows:" + "As well as the id of the KItem we can do it as follows:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "UUID('5cb3a95e-4aac-434d-81ba-7c242fd245aa')" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.id" ] @@ -299,88 +144,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ktype:\n", - " id: specimen\n", - " name: Specimen\n", - " webform_schema_id: 21164fb6-cc45-4e08-8f8b-467a749df54b\n", - " webform_schema:\n", - " id: 21164fb6-cc45-4e08-8f8b-467a749df54b\n", - " name: Specimen\n", - " spec:\n", - " semantics_enabled: true\n", - " sections_enabled: false\n", - " class_mapping:\n", - " - https://w3id.org/pmd/co/Specimen\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " inputs:\n", - " - id: id6c76bbffe7ca78\n", - " label: Width\n", - " widget: Number\n", - " required: false\n", - " hidden: false\n", - " ignore: false\n", - " select_options: []\n", - " measurement_unit:\n", - " label: Millimetre\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " namespace: http://qudt.org/vocab/unit\n", - " relation_mapping:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0\n", - " multiple_selection: false\n", - " knowledge_type:\n", - " - null\n", - " range_options:\n", - " min: 0\n", - " max: 1\n", - " step: 0.1\n", - " range: false\n", - " - id: id717d07130a7618\n", - " label: Length\n", - " widget: Slider\n", - " required: false\n", - " hidden: false\n", - " ignore: false\n", - " select_options: []\n", - " measurement_unit:\n", - " label: Millimetre\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " namespace: http://qudt.org/vocab/unit\n", - " relation_mapping:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_cd2cd0de_e0cc_4ef1_b27e_2e88db027bac\n", - " relation_mapping_extra:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0\n", - " multiple_selection: false\n", - " range_options:\n", - " min: 0\n", - " max: 1\n", - " step: 0.1\n", - " range: true\n", - " hidden: false\n", - " created_at: '2025-07-21T09:24:48.597715'\n", - " updated_at: '2025-07-21T13:56:38.225444'\n", - " created_at: '2025-07-21T09:20:12.746430'\n", - " updated_at: '2025-07-21T09:24:58.495720'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.ktype" ] @@ -394,20 +160,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.is_a(dsms.ktypes.Specimen)" ] @@ -421,42 +176,15 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "@prefix ns1: .\n", - "@prefix ns2: .\n", - "@prefix rdfs: .\n", - "@prefix xsd: .\n", - "\n", - " a ;\n", - " rdfs:label \"Specimen123\"^^xsd:string ;\n", - " ns1:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ,\n", - " ,\n", - " .\n", - "\n", - " a ns1:EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0 ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value \"0.5\"^^xsd:float .\n", - "\n", - " a ns1:EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0 ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value \"0.2\"^^xsd:float .\n", - "\n", - " a ns1:EMMO_cd2cd0de_e0cc_4ef1_b27e_2e88db027bac ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value \"0.1\"^^xsd:float .\n", - "\n", - "\n" - ] - } - ], + "outputs": [], "source": [ - "print(item.subgraph.serialize())" + "try:\n", + " print(item.subgraph.serialize())\n", + "except ValueError as e:\n", + " print(f\"Note: RDF subgraph is generated asynchronously.\")\n", + " print(f\"It may not be available immediately after creation.\")" ] }, { @@ -468,42 +196,26 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.0005" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.custom_properties.Width.convert_to(\"m\")" + "try:\n", + " item.custom_properties.Width.convert_to(\"m\")\n", + "except ValueError as e:\n", + " print(f\"Unit conversion not available: {e}\")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[0.0001, 0.0002]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.custom_properties.Length.convert_to(\"m\")" + "try:\n", + " item.custom_properties.Length.convert_to(\"m\")\n", + "except ValueError as e:\n", + " print(f\"Unit conversion not available: {e}\")" ] }, { @@ -515,30 +227,48 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'Width': 0.5, 'Length': [0.1, 0.2]}" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.custom_properties.model_dump(flat=True)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.x. Setting access properties\n", + "\n", + "Access control is defined via `access_properties`, which assigns roles to specific users and groups. The available roles are `MEMBER` (read only), `CONTRIBUTOR` (read and update), `OWNER` (read, update, delete, manage), and `ADMIN` (same as OWNER)." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from dsms.knowledge.properties.access import KItemAccessProperties, Role\n", + "\n", + "# Look up the current user to demonstrate role assignment\n", + "uname = dsms.config.username\n", + "if hasattr(uname, \"get_secret_value\"):\n", + " uname = uname.get_secret_value()\n", + "current_user = dsms.users.by_username.get(uname)\n", + "\n", + "item.access_properties = KItemAccessProperties(\n", + " user_access=[{\"user_id\": current_user.id, \"role\": Role.OWNER}],\n", + ")\n", + "dsms.commit()" + ], + "outputs": [], + "execution_count": null + }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", - "Now you can check if the particular kitem is in the list of KItems. This can be done either by using the command:\n", + "Now you can check if the particular KItem is in the list of KItems. This can be done either by using the command:\n", " `\n", " dsms.kitems\n", " `\n", diff --git a/docs/dsms_sdk/tutorials/3_updating.ipynb b/docs/dsms_sdk/tutorials/3_updating.ipynb new file mode 100644 index 0000000..a0464e2 --- /dev/null +++ b/docs/dsms_sdk/tutorials/3_updating.ipynb @@ -0,0 +1,235 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Updating KItems with the SDK\n", + "\n", + "In this tutorial we see how to update existing KItems." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "\n", + "Now let us import the needed classes and functions for this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dsms import DSMS, KItem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now source the environmental variables from an `.env` file and start the DSMS-session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let us get the KItem we created in the [2nd tutorial: Creation of KItems](2_creation.ipynb)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item = KItem(\n", + " name=\"Specimen123\",\n", + " ktype_id=dsms.ktypes.Specimen,\n", + " custom_properties={\"Width\": 0.5, \"Length\": 0.15},\n", + ")\n", + "dsms.add(item)\n", + "dsms.commit()\n", + "item" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2. Updating KItems" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we would like to update the properties of our KItem we created previously.\n", + "\n", + "Depending on the schema of each property (see [DSMS KItem Schema](../dsms_kitem_schema.md)), `list` accumulation (`+=` or `-=`), e.g. for the `annotations`, `attachments`, `external_link`, etc. \n", + "\n", + "**NOTE**: using `append` or `extend` will not validate the pydantic model of the respective fields and hence will cause an error during committing. Hence, always use `+=`, `=` or `-=`.\n", + "\n", + "Other properties which are not `list`-like can be simply set by attribute-assignment (e.g. `name`, `slug`, `ktype_id`, etc)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item.name = \"Specimen-123\"\n", + "item.custom_properties.Width = 1\n", + "item.attachments += [\"testfile.txt\"]\n", + "item.annotations += [\"https://w3id.org/pmd/co/Specimen\"]\n", + "item.external_links += [\n", + " {\"url\": \"http://specimens.org\", \"label\": \"specimen-link\"}\n", + "]\n", + "item.contacts += [{\"name\": \"Specimen preparation\", \"email\": \"specimenpreparation@group.mail\"}]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dsms.add(item)\n", + "dsms.commit()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see now that the local system path of the attachment is changed to a simple file name, which means that the upload was successful. If not so, an error would have been thrown during the `commit`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the updates when we print the item:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.3. Updating access properties\n", + "\n", + "Access control entries can be updated in the same way as other KItem properties." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from dsms.knowledge.properties.access import KItemAccessProperties, Role\n", + "\n", + "# Look up the current user to demonstrate role assignment\n", + "uname = dsms.config.username\n", + "if hasattr(uname, \"get_secret_value\"):\n", + " uname = uname.get_secret_value()\n", + "current_user = dsms.users.by_username.get(uname)\n", + "\n", + "item.access_properties = KItemAccessProperties(\n", + " user_access=[{\"user_id\": current_user.id, \"role\": Role.CONTRIBUTOR}],\n", + ")\n", + "dsms.commit()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Furthermore we can also download the file we uploaded again:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for file in item.attachments:\n", + " download = file.download()\n", + "\n", + " print(\"\\t\\t\\t Downloaded file:\", file.name)\n", + " print(\"|------------------------------------Beginning of file------------------------------------|\")\n", + " print(download)\n", + " print(\"|---------------------------------------End of file---------------------------------------|\\n\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Clean up the tutorial item\n", + "del dsms[item]\n", + "dsms.commit()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/dsms_sdk/tutorials/3_updation.ipynb b/docs/dsms_sdk/tutorials/3_updation.ipynb deleted file mode 100644 index 178e205..0000000 --- a/docs/dsms_sdk/tutorials/3_updation.ipynb +++ /dev/null @@ -1,360 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. Updating KItems with the SDK\n", - "\n", - "In this tutorial we see how to update existing Kitems." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.1. Setting up\n", - "\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "\n", - "Now let us import the needed classes and functions for this tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from dsms import DSMS" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now source the environmental variables from an `.env` file and start the DSMS-session." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "dsms = DSMS(env=\".env\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now lets get the kitem we created in the [2nd tutorial : Creation of Kitems](2_creation.ipynb)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "item = dsms[\"5cb3a95e-4aac-434d-81ba-7c242fd245aa\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 5cb3a95e-4aac-434d-81ba-7c242fd245aa\n", - " name: Specimen123\n", - " ktype_id: specimen\n", - " slug: specimen123-5cb3a95e\n", - " annotations: []\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:29:01.826691\n", - " updated_at: 2025-08-13 14:29:01.826691\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 0.5\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2. Updating Kitems" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we would like to update the properties of our KItem we created previously.\n", - "\n", - "Depending on the schema of each property (see [DSMS KItem Schema](../dsms_kitem_schema.md)), `list`accumulation (`+=` or `-=`), e.g. for the `annotations`, `attachments`, `external_link`, etc. \n", - "\n", - "**NOTE**: using `append` or `extend` will not validate the pydantic model of the respective fields and hence will cause an error during commiting. Hence, always use `+=`, `=` or `-=`.\n", - "\n", - "Other properties which are not `list`-like can be simply set by attribute-assignment (e.g. `name`, `slug`, `ktype_id`, etc)." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "item.name = \"Specimen-123\"\n", - "item.custom_properties.Width = 1\n", - "item.attachments += [\"testfile.txt\"]\n", - "item.annotations += [\"https://w3id.org/pmd/co/Specimen\"]\n", - "item.external_links += [\n", - " {\"url\": \"http://specimens.org\", \"label\": \"specimen-link\"}\n", - "]\n", - "item.contacts += [{\"name\": \"Specimen preparation\", \"email\": \"specimenpreparation@group.mail\"}]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "dsms.add(item)\n", - "dsms.commit()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see now that the local system path of the attachment is changed to a simply file name, which means that the upload was successful. If not so, an error would have been thrown during the `commit`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see the updates when we print the item:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 5cb3a95e-4aac-434d-81ba-7c242fd245aa\n", - " name: Specimen-123\n", - " ktype_id: specimen\n", - " slug: specimen123-5cb3a95e\n", - " annotations:\n", - " - iri: https://w3id.org/pmd/co/Specimen\n", - " label: Specimen\n", - " namespace: https://w3id.org/pmd/co\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " - name: testfile.txt\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts:\n", - " - name: Specimen preparation\n", - " email: specimenpreparation@group.mail\n", - " created_at: 2025-08-13 14:29:01.826691\n", - " updated_at: 2025-08-13 14:30:26.915234\n", - " external_links:\n", - " - label: specimen-link\n", - " url: http://specimens.org\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 1\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Furthermore we can also download the file we uploaded again:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\t\t\t Downloaded file: subgraph.ttl\n", - "|------------------------------------Beginning of file------------------------------------|\n", - "@prefix ns1: .\n", - "@prefix ns2: .\n", - "@prefix rdfs: .\n", - "@prefix xsd: .\n", - "\n", - " a ;\n", - " rdfs:label \"Specimen-123\"^^xsd:string ;\n", - " ns1:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ,\n", - " ,\n", - " .\n", - "\n", - " a ns1:EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0 ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value 1 .\n", - "\n", - " a ns1:EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0 ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value \"0.2\"^^xsd:float .\n", - "\n", - " a ns1:EMMO_cd2cd0de_e0cc_4ef1_b27e_2e88db027bac ;\n", - " ns2:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI ;\n", - " ns2:value \"0.1\"^^xsd:float .\n", - "\n", - "\n", - "|---------------------------------------End of file---------------------------------------|\n", - "\n", - "\n", - "\t\t\t Downloaded file: testfile.txt\n", - "|------------------------------------Beginning of file------------------------------------|\n", - "This is a specimen preparation protocol!\n", - "\n", - "|---------------------------------------End of file---------------------------------------|\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for file in item.attachments:\n", - " download = file.download()\n", - "\n", - " print(\"\\t\\t\\t Downloaded file:\", file.name)\n", - " print(\"|------------------------------------Beginning of file------------------------------------|\")\n", - " print(download)\n", - " print(\"|---------------------------------------End of file---------------------------------------|\\n\\n\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/dsms_sdk/tutorials/4_deletion.ipynb b/docs/dsms_sdk/tutorials/4_deletion.ipynb index fa033ad..ac6a3e1 100644 --- a/docs/dsms_sdk/tutorials/4_deletion.ipynb +++ b/docs/dsms_sdk/tutorials/4_deletion.ipynb @@ -4,29 +4,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Deleting KItems with the SDK\n", - "\n", - "In this tutorial we see how to delete new Kitems and their properties." + "# 4. Deleting KItems with the SDK\n\nIn this tutorial we see how to delete KItems and their properties." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 4.1. Setting up\n", - "\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "Now let us import the needed classes and functions for this tutorial." + "### 4.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nNow let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from dsms import DSMS" + "from dsms import DSMS, KItem\n", + "from dsms.knowledge.properties.contacts import ContactInfo\n", + "from dsms.knowledge.properties.affiliations import Affiliation" ] }, { @@ -38,11 +34,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { @@ -54,11 +51,24 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "item = dsms[\"5cb3a95e-4aac-434d-81ba-7c242fd245aa\"]" + "# Create a Specimen with properties to demonstrate deletion\n", + "item = KItem(\n", + " name=\"SpecimenToDelete\",\n", + " ktype_id=dsms.ktypes.Specimen,\n", + " custom_properties={\"Width\": 0.5, \"Length\": 0.15},\n", + " annotations=[\"https://w3id.org/pmd/co/Specimen\"],\n", + " attachments=[\"testfile.txt\"],\n", + " external_links=[{\"url\": \"https://example.com\", \"label\": \"Example link\"}],\n", + " contacts=[ContactInfo(name=\"Tutorial Contact\", email=\"contact@example.com\")],\n", + " affiliations=[Affiliation(name=\"Example Institute\")],\n", + ")\n", + "dsms.add(item)\n", + "dsms.commit()\n", + "item" ] }, { @@ -83,22 +93,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "contact:\n", - " name: Specimen preparation\n", - " email: specimenpreparation@group.mail" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.attachments.pop(0)\n", "item.annotations.pop(0)\n", @@ -115,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -131,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -139,6 +136,13 @@ "dsms.commit()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -148,74 +152,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 5cb3a95e-4aac-434d-81ba-7c242fd245aa\n", - " name: Specimen-123\n", - " ktype_id: specimen\n", - " slug: specimen123-5cb3a95e\n", - " annotations: []\n", - " attachments:\n", - " - name: testfile.txt\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts:\n", - " - name: Specimen preparation\n", - " email: specimenpreparation@group.mail\n", - " created_at: 2025-08-13 14:29:01.826691\n", - " updated_at: 2025-08-13 14:30:26.915234\n", - " external_links:\n", - " - label: specimen-link\n", - " url: http://specimens.org\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 1\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item" ] @@ -229,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -246,13 +185,22 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dsms.commit()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dsms.kitems" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/dsms_sdk/tutorials/5_search.ipynb b/docs/dsms_sdk/tutorials/5_search.ipynb index 9cfd7c0..c123e01 100644 --- a/docs/dsms_sdk/tutorials/5_search.ipynb +++ b/docs/dsms_sdk/tutorials/5_search.ipynb @@ -4,25 +4,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 5. Searching KItems with the SDK\n", - "\n", - "In this tutorial we see how to search existing Kitems" + "# 5. Searching KItems with the SDK\n\nIn this tutorial we see how to search existing KItems" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 5.1. Setting up\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "\n", - "Now let us import the needed classes and functions for this tutorial." + "### 5.1. Setting up\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\n\nNow let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -38,11 +32,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { @@ -56,235 +51,37 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this section, we would like to search for specfic KItems we created in the DSMS.\n", - "\n", - "For this purpose, we will firstly create some KItems and apply the `search`-method on the `DSMS`-object later on in order to find them again in the DSMS.\n", - "\n", - "We also want to demonstrate here, that we can link KItems to each other in order to find e.g. a related item of type `DatasetCatalog`. For this strategy, we are using the `linked_kitems`- attribute and the `id` of the item which we would like to link.\n", - "\n", - "The procedure looks like this:" + "In this section, we would like to search for specific KItems we created in the DSMS.\n\nFor this purpose, we will firstly create some KItems and apply the `search`-method on the `DSMS`-object later on in order to find them again in the DSMS.\n\nWe also want to demonstrate here, that we can link KItems to each other in order to find e.g. a related item of type `DatasetCatalog`. For this strategy, we are using the `linked_kitems`- attribute and the `id` of the item which we would like to link.\n\nThe procedure looks like this:" ] }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:406: UserWarning: A flat dictionary was provided for custom properties.\n", - " Will be transformed into `KItemCustomPropertiesModel`.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:306: UserWarning: Found a to be linked instead of an . Will link it with the default relationship 'dcterms:haspart'.\n", - " warnings.warn(\n", - "/usr/local/lib/python3.11/site-packages/pydantic/main.py:463: UserWarning: Pydantic serializer warnings:\n", - " PydanticSerializationUnexpectedValue(Expected `str` - serialized value may not be as expected [input_value=False, input_type=bool])\n", - " return self.__pydantic_serializer__.to_python(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Producer`. Cannot check if value is of correct type.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Room Number`. Cannot check if value is of correct type.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Description`. Cannot check if value is of correct type.\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "item1 = KItem(\n", - " name=\"Machine-1\",\n", - " ktype_id=dsms.ktypes.Testingmachine,\n", - " annotations=[\"https://w3id.org/steel/ProcessOntology/TestingMachine\"],\n", - " custom_properties={\"Producer\": \"TestingLab GmBH\",\n", - " \"Room Number\": \"A404\",\n", - " \"Description\": \"Bending Test Machine\"\n", - " }\n", - ")\n", - "\n", - "item2 = KItem(\n", - " name=\"Machine-2\",\n", - " ktype_id=dsms.ktypes.Testingmachine,\n", - " annotations=[\"https://w3id.org/steel/ProcessOntology/TestingMachine\"],\n", - " custom_properties={\"Producer\": \"StressStrain GmBH\",\n", - " \"Room Number\": \"B500\",\n", - " \"Description\": \"Compression Test Machine\"\n", - " }\n", - ")\n", - "\n", - "item3 = KItem(\n", - " name=\"Specimen-1\", \n", - " ktype_id=dsms.ktypes.Specimen,\n", - " linked_kitems=[item1],\n", - " annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n", - " custom_properties = {\n", - " \"Width\": 0.5,\n", - " \"Length\": [0.1, 0.2],\n", - " }\n", - "\n", - ")\n", - "item4 = KItem(\n", - " name=\"Specimen-2\",\n", - " ktype_id=dsms.ktypes.Specimen,\n", - " linked_kitems=[item2],\n", - " annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n", - " custom_properties = {\n", - " \"Width\": 0.8,\n", - " \"Length\": [0.8, 0.9],\n", - " }\n", - ")\n", - "\n", - "item5 = KItem(\n", - " name=\"Research Institute ABC\",\n", - " ktype_id=dsms.ktypes.Organization,\n", - " linked_kitems=[item1,item2],\n", - " annotations=[\"www.researchBACiri.org/foo\"],\n", - ")\n", - "\n", - "dsms.add([item1, item2, item3, item4, item5])\n", - "dsms.commit()" + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item1 = KItem(\n name=\"Machine-1\",\n ktype_id=dsms.ktypes.MeasurementDevice\n)\n\nitem2 = KItem(\n name=\"Machine-2\",\n ktype_id=dsms.ktypes.MeasurementDevice\n)\n\nitem3 = KItem(\n name=\"Specimen-1\", \n ktype_id=dsms.ktypes.Specimen,\n linked_kitems=[item1],\n annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n custom_properties = {\n \"Width\": 0.5,\n \"Length\": 0.15,\n }\n\n)\nitem4 = KItem(\n name=\"Specimen-2\",\n ktype_id=dsms.ktypes.Specimen,\n linked_kitems=[item2],\n annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n custom_properties = {\n \"Width\": 0.8,\n \"Length\": 0.85,\n }\n)\n\nitem5 = KItem(\n name=\"Research Institute ABC\",\n ktype_id=dsms.ktypes.Organization,\n linked_kitems=[item1,item2],\n annotations=[\"www.researchBACiri.org/foo\"],\n)\n\ndsms.add([item1, item2, item3, item4, item5])\ndsms.commit()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "\n", - "

Note : Here in this tutorial, we use dsms.search with `limit=1` to maintain readability but the user can adjust the variable `limit` as per requirement.

\n", - "\n", - "\n", - "Now, we are apply to search for e.g. kitems of type `TestingMachine`:" + "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n

Note : Here in this tutorial, we use dsms.search with `limit=2` to maintain readability but the user can adjust the variable `limit` as per requirement.

\n\n\nNow, we can search for e.g. KItems of type `MeasurementDevice`:" ] }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "hits:\n", - "- kitem:\n", - " id: 14e9a531-7f91-4dd5-a72d-f8753a84d1ab\n", - " name: Specimen123\n", - " ktype_id: specimen\n", - " slug: specimen123-14e9a531\n", - " annotations: []\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-12 14:41:51.234220\n", - " updated_at: 2025-08-12 14:41:51.234220\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 0.5\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []\n", - " fuzzy: false\n", - "- kitem:\n", - " id: 1f3694d3-4624-45b8-9182-3bf88c6511f9\n", - " name: Specimen-123\n", - " ktype_id: specimen\n", - " slug: specimen123-1f3694d3\n", - " annotations:\n", - " - iri: https://w3id.org/pmd/co/Specimen\n", - " label: Specimen\n", - " namespace: https://w3id.org/pmd/co\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " - name: testfile.txt\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts:\n", - " - name: Specimen preparation\n", - " email: specimenpreparation@group.mail\n", - " created_at: 2025-08-13 13:08:52.481587\n", - " updated_at: 2025-08-13 14:23:47.730855\n", - " external_links:\n", - " - label: specimen-link\n", - " url: http://specimens.org\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 1\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []\n", - " fuzzy: false\n", - "total_count: 5\n", - "\n", - "Name of the first kitem:\n", - "Specimen123\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "result = dsms.search(ktypes=[dsms.ktypes.Specimen], limit=2)\n", "print(result)\n", @@ -296,141 +93,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "... and for all of type `Organization` and `Testingmachine`:" + "... and for all of type `Organization` and `MeasurementDevice`:" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "kitem:\n", - " id: 4d587532-4275-4f4f-9d19-e199d9452d13\n", - " name: Research Institute ABC\n", - " ktype_id: organization\n", - " slug: researchinstituteabc-4d587532\n", - " annotations:\n", - " - iri: www.researchBACiri.org/foo\n", - " label: foo\n", - " namespace: www.researchBACiri.org\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " linked_kitems:\n", - " - is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " - is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:33.681221\n", - " updated_at: 2025-08-13 14:33:33.681221\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " rdf_exists: true\n", - " contexts: []\n", - "\n", - "fuzziness: False\n", - "\n", - "\n", - "kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad\n", - " annotations:\n", - " - iri: https://w3id.org/steel/ProcessOntology/TestingMachine\n", - " label: TestingMachine\n", - " namespace: https://w3id.org/steel/ProcessOntology\n", - " attachments: []\n", - " linked_kitems:\n", - " - is_incoming: true\n", - " label: Has Part\n", - " kitem:\n", - " id: 4d587532-4275-4f4f-9d19-e199d9452d13\n", - " name: Research Institute ABC\n", - " ktype_id: organization\n", - " slug: researchinstituteabc-4d587532\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " - is_incoming: true\n", - " label: Has Part\n", - " kitem:\n", - " id: d1ce4f7e-223b-4086-b5e5-11017f47270d\n", - " name: Specimen-1\n", - " ktype_id: specimen\n", - " slug: specimen-1-d1ce4f7e\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:29.742517\n", - " updated_at: 2025-08-13 14:33:29.742517\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id17550956094374867vv\n", - " name: Misc\n", - " entries:\n", - " - id: id1755095609437i4mybj\n", - " type: Text\n", - " label: Producer\n", - " value: TestingLab GmBH\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id1755095609437hd07kh\n", - " type: Text\n", - " label: Room Number\n", - " value: A404\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id17550956094377bpur4\n", - " type: Text\n", - " label: Description\n", - " value: Bending Test Machine\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: false\n", - " contexts: []\n", - "\n", - "fuzziness: False\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for result in dsms.search(ktypes=[dsms.ktypes.Organization, dsms.ktypes.Testingmachine], limit=2):\n", - " print(result.kitem)\n", - " print(\"fuzziness: \", result.fuzzy)\n", - " print(\"\\n\")\n", - " " + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for result in dsms.search(ktypes=[dsms.ktypes.Organization, dsms.ktypes.MeasurementDevice], limit=2):\n print(result.kitem)\n print(\"fuzziness: \", result.fuzzy)\n print(\"\\n\")\n " ] }, { @@ -442,77 +114,9 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "kitem:\n", - " id: 1f3694d3-4624-45b8-9182-3bf88c6511f9\n", - " name: Specimen-123\n", - " ktype_id: specimen\n", - " slug: specimen123-1f3694d3\n", - " annotations:\n", - " - iri: https://w3id.org/pmd/co/Specimen\n", - " label: Specimen\n", - " namespace: https://w3id.org/pmd/co\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " - name: testfile.txt\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts:\n", - " - name: Specimen preparation\n", - " email: specimenpreparation@group.mail\n", - " created_at: 2025-08-13 13:08:52.481587\n", - " updated_at: 2025-08-13 14:23:47.730855\n", - " external_links:\n", - " - label: specimen-link\n", - " url: http://specimens.org\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", - " entries:\n", - " - id: id6c76bbffe7ca78\n", - " type: Number\n", - " label: Width\n", - " value: 1\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " - id: id717d07130a7618\n", - " type: Slider\n", - " label: Length\n", - " value:\n", - " - 0.1\n", - " - 0.2\n", - " measurementUnit:\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " label: Millimetre\n", - " symbol: null\n", - " namespace: http://qudt.org/vocab/unit\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []\n", - "fuzzy: false\n", - "\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "for result in dsms.search(query=\"Specimen-1\", ktypes=[dsms.ktypes.Specimen], allow_fuzzy=False, limit=1):\n", " print(result)" @@ -527,58 +131,9 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "kitem:\n", - " id: 4d587532-4275-4f4f-9d19-e199d9452d13\n", - " name: Research Institute ABC\n", - " ktype_id: organization\n", - " slug: researchinstituteabc-4d587532\n", - " annotations:\n", - " - iri: www.researchBACiri.org/foo\n", - " label: foo\n", - " namespace: www.researchBACiri.org\n", - " attachments:\n", - " - name: subgraph.ttl\n", - " linked_kitems:\n", - " - is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " - is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:33.681221\n", - " updated_at: 2025-08-13 14:33:33.681221\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " rdf_exists: true\n", - " contexts: []\n", - "fuzzy: false\n", - "\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "for result in dsms.search(\n", " ktypes=[dsms.ktypes.Organization], annotations=[\"www.researchBACiri.org/foo\"], allow_fuzzy=False, limit=1\n", @@ -590,47 +145,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 5.3. Fetching linked KItems from a KItem" + "### 5.3. Fetching linked KItems from a KItem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the beginning under **5.1** we created some kitems and linked each other. Now we want to fetch the linked kitems and display them to the user. For this we use the `linked_kitems` attribute." + "In section **5.2** we created some KItems and linked each other. Now we want to fetch the linked KItems and display them to the user. For this we use the `linked_kitems` attribute." ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "- is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " iri: http://purl.org/dc/terms/hasPart\n", - "- is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad\n", - " iri: http://purl.org/dc/terms/hasPart" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "item5.linked_kitems" ] @@ -644,69 +173,9 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " annotations:\n", - " - iri: https://w3id.org/steel/ProcessOntology/TestingMachine\n", - " label: TestingMachine\n", - " namespace: https://w3id.org/steel/ProcessOntology\n", - " attachments: []\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:30.358137\n", - " updated_at: 2025-08-13 14:33:30.358137\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id1755095609461z4tdh1\n", - " name: Misc\n", - " entries:\n", - " - id: id1755095609461bqgrqv\n", - " type: Text\n", - " label: Producer\n", - " value: StressStrain GmBH\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id17550956094616vktvr\n", - " type: Text\n", - " label: Room Number\n", - " value: B500\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id1755095609461dhszfo\n", - " type: Text\n", - " label: Description\n", - " value: Compression Test Machine\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: false\n", - " contexts: []" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "item5.linked_kitems[0].fetch()" ] @@ -720,98 +189,18 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'http://purl.org/dc/terms/hasPart': [kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2,\n", - " kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad]}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "item5.linked_kitems.by_relation" ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " annotations:\n", - " - iri: https://w3id.org/steel/ProcessOntology/TestingMachine\n", - " label: TestingMachine\n", - " namespace: https://w3id.org/steel/ProcessOntology\n", - " attachments: []\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:30.358137\n", - " updated_at: 2025-08-13 14:33:30.358137\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id1755095609461z4tdh1\n", - " name: Misc\n", - " entries:\n", - " - id: id1755095609461bqgrqv\n", - " type: Text\n", - " label: Producer\n", - " value: StressStrain GmBH\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id17550956094616vktvr\n", - " type: Text\n", - " label: Room Number\n", - " value: B500\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id1755095609461dhszfo\n", - " type: Text\n", - " label: Description\n", - " value: Compression Test Machine\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: false\n", - " contexts: []" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "item5.linked_kitems.by_relation[\"http://purl.org/dc/terms/hasPart\"][0].fetch()\n" ] @@ -825,33 +214,9 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{ktype:\n", - " id: testingmachine\n", - " name: TestingMachine\n", - " created_at: '2025-07-22T12:41:50.505566'\n", - " updated_at: '2025-07-22T12:41:50.505566': [kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2,\n", - " kitem:\n", - " id: 0f89d6ad-3446-4d8a-9f27-39a69bcffeee\n", - " name: Machine-1\n", - " ktype_id: testingmachine\n", - " slug: machine-1-0f89d6ad]}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "item5.linked_kitems.by_ktype" ] @@ -865,83 +230,49 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 2cf0aea2-b4c2-40b0-a786-bcf00ddb1991\n", - " name: Machine-2\n", - " ktype_id: testingmachine\n", - " slug: machine-2-2cf0aea2\n", - " annotations:\n", - " - iri: https://w3id.org/steel/ProcessOntology/TestingMachine\n", - " label: TestingMachine\n", - " namespace: https://w3id.org/steel/ProcessOntology\n", - " attachments: []\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 14:33:30.358137\n", - " updated_at: 2025-08-13 14:33:30.358137\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id1755095609461z4tdh1\n", - " name: Misc\n", - " entries:\n", - " - id: id1755095609461bqgrqv\n", - " type: Text\n", - " label: Producer\n", - " value: StressStrain GmBH\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id17550956094616vktvr\n", - " type: Text\n", - " label: Room Number\n", - " value: B500\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id1755095609461dhszfo\n", - " type: Text\n", - " label: Description\n", - " value: Compression Test Machine\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: false\n", - " contexts: []" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item5.linked_kitems.by_ktype[dsms.ktypes.Testingmachine][0].fetch()" + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "item5.linked_kitems.by_ktype[dsms.ktypes.MeasurementDevice][0].fetch()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.4. Filtering by context and attachment type\n\n`DSMS.search()` also supports filtering by context membership and attachment file extensions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Find KItems that belong to a specific context (pass the context KItem's ID as a string)\n", + "results = dsms.search(contexts=[\"\"])\n", + "\n", + "# Find KItems that have PDF attachments\n", + "results = dsms.search(attachment_extensions=[\".pdf\"])\n", + "\n", + "# Combine filters\n", + "results = dsms.search(\n", + " query=\"tensile\",\n", + " attachment_extensions=[\".csv\", \".xlsx\"],\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Clean up the DSMS from the tutortial:" + "Clean up the DSMS from the tutorial:" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/docs/dsms_sdk/tutorials/6_apps.ipynb b/docs/dsms_sdk/tutorials/6_apps.ipynb index 375b3a7..7d515a9 100644 --- a/docs/dsms_sdk/tutorials/6_apps.ipynb +++ b/docs/dsms_sdk/tutorials/6_apps.ipynb @@ -13,12 +13,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 6.1: Setting up\n", - "\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "\n", - "Now let us import the needed classes and functions for this tutorial." + "### 6.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\n\nNow let us import the needed classes and functions for this tutorial." ] }, { @@ -44,14 +39,15 @@ "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 6.1. Investigating Available Apps" + "### 6.2. Investigating Available Apps" ] }, { @@ -118,35 +114,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 6.2 Create a new app config and apply it to a KItem" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 6.2.1 Arbitrary python code" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To be defined." + "### 6.3. Create a new app config and apply it to a KItem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 6.2.2 - Data2RDF" + "### 6.3.1. Data2RDF" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### 6.2.2.1 Prepare app and its config" + "#### 6.3.1.1 Prepare app and its config" ] }, { @@ -283,7 +265,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we instanciate the new app config:" + "Now we instantiate the new app config:" ] }, { @@ -379,11 +361,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we would like to apply the app config to a KItem. The set the `triggerUponUpload` must be set to `True` so that the app is triggered automatically when we upload an attachment.\n", - "\n", - "Additionally, we must tell the file extension for which the upload shall be triggered. Here it is `.csv`.\n", - "\n", - "We also want to generate a qr code as avatar for the KItem with `avatar={\"include_qr\": True}`." + "Now we would like to apply the app config to a KItem. `triggerUponUpload` must be set to `True` so that the app is triggered automatically when we upload an attachment.\n\nAdditionally, we must tell the file extension for which the upload shall be triggered. Here it is `.csv`.\n\nWe also want to generate a qr code as avatar for the KItem with `avatar={\"include_qr\": True}`." ] }, { @@ -463,7 +441,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### 6.2.2.2 Get results" + "#### 6.3.1.2 Get results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control." ] }, { @@ -611,7 +596,11 @@ } ], "source": [ - "print(item.subgraph.serialize())" + "try:\n", + " print(item.subgraph.serialize())\n", + "except ValueError as e:\n", + " print(f\"Note: RDF subgraph is generated asynchronously.\")\n", + " print(f\"It may not be available immediately after creation.\")" ] }, { @@ -647,14 +636,18 @@ } ], "source": [ - "item.dataframe.StandardForce.convert_to(\"N\")" + "try:\n", + " item.dataframe.StandardForce.convert_to(\"N\")\n", + "except (AttributeError, ValueError) as e:\n", + " print(f\"Note: dataframe may not be available yet (app processing is asynchronous).\")\n", + " print(f\"Error: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### 6.2.2.3 Manipulate dataframe" + "#### 6.3.1.3 Manipulate dataframe" ] }, { @@ -780,7 +773,11 @@ } ], "source": [ - "item.dataframe.to_df()" + "try:\n", + " item.dataframe.to_df()\n", + "except (AttributeError, ValueError) as e:\n", + " print(f\"Note: dataframe not available (asynchronous processing).\")\n", + " print(f\"Error: {e}\")" ] }, { @@ -831,8 +828,12 @@ } ], "source": [ - "for column in item.dataframe:\n", - " print(\"column:\", column.name, \",\\n\", \"data:\", column.get())\n" + "try:\n", + " for column in item.dataframe:\n", + " print(\"column:\", column.name, \",\\n\", \"data:\", column.get())\n", + "except (AttributeError, TypeError) as e:\n", + " print(f\"Note: dataframe not available yet.\")\n", + " print(f\"Error: {e}\")" ] }, { @@ -848,15 +849,19 @@ "metadata": {}, "outputs": [], "source": [ - "new_df = item.dataframe.to_df().drop(['TestTime'], axis=1)\n", - "item.dataframe = new_df" + "try:\n", + " new_df = item.dataframe.to_df().drop(['TestTime'], axis=1)\n", + " item.dataframe = new_df\n", + "except (AttributeError, TypeError) as e:\n", + " print(f\"Note: dataframe not available yet.\")\n", + " print(f\"Error: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### 6.2.2.4 Run app on demand" + "#### 6.3.1.4 Run app on demand" ] }, { @@ -876,10 +881,15 @@ "metadata": {}, "outputs": [], "source": [ - "job = item.apps.by_title[\"data2rdf\"].run(\n", - " attachment_name=\"dummy_data.csv\",\n", - " expose_sdk_config=True\n", - ")" + "try:\n", + " job = item.apps.by_title[\"data2rdf\"].run(\n", + " attachment_name=\"dummy_data.csv\",\n", + " expose_sdk_config=True\n", + " )\n", + "except RuntimeError as e:\n", + " print(f\"Note: app execution requires appropriate permissions.\")\n", + " print(f\"Error: {e}\")\n", + " job = None" ] }, { @@ -910,7 +920,7 @@ } ], "source": [ - "job.status" + "print(job.status) if job else print(\"Job not available.\")" ] }, { @@ -937,7 +947,7 @@ } ], "source": [ - "job.logs" + "print(job.logs) if job else print(\"Job not available.\")" ] }, { @@ -953,11 +963,16 @@ "metadata": {}, "outputs": [], "source": [ - "job = item.apps.by_title[\"data2rdf\"].run(\n", - " attachment_name=\"dummy_data.csv\",\n", - " expose_sdk_config=True,\n", - " wait=False,\n", - ")" + "try:\n", + " job = item.apps.by_title[\"data2rdf\"].run(\n", + " attachment_name=\"dummy_data.csv\",\n", + " expose_sdk_config=True,\n", + " wait=False,\n", + " )\n", + "except RuntimeError as e:\n", + " print(f\"Note: app execution requires appropriate permissions.\")\n", + " print(f\"Error: {e}\")\n", + " job = None" ] }, { @@ -1125,14 +1140,18 @@ } ], "source": [ - "while True:\n", - " time.sleep(1)\n", - " print(job.status)\n", - " print(\"Current logs:\")\n", - " print(job.logs)\n", - " print(\"\\n\")\n", - " if job.status.phase != \"Running\":\n", - " break" + "if job:\n", + " import time\n", + " while True:\n", + " time.sleep(1)\n", + " print(job.status)\n", + " print(\"Current logs:\")\n", + " print(job.logs)\n", + " print(\"\\n\")\n", + " if job.status.phase != \"Running\":\n", + " break\n", + "else:\n", + " print(\"Job not available.\")" ] }, { diff --git a/docs/dsms_sdk/tutorials/7_ktypes.ipynb b/docs/dsms_sdk/tutorials/7_ktypes.ipynb index 95d68bd..6ea57a0 100644 --- a/docs/dsms_sdk/tutorials/7_ktypes.ipynb +++ b/docs/dsms_sdk/tutorials/7_ktypes.ipynb @@ -11,16 +11,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 7.1. Setting up\n", - "\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "Import the needed classes and functions." + "## 7.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nImport the needed classes and functions." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -36,210 +32,56 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 7.2. Create KTypes\n", - "\n", - "A Ktype can have a webform schema - for e.g. data properties or a process schema for clustering KItems into a context.\n", - "\n", - "The webform and process schema of a ktype may look as follows:" + "> **Note:** Some fields visible in KType and KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0. They remain in the model for backward compatibility. For KType management, prefer the v2 API methods described in section 7.5 of this tutorial." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7.2. Create KTypes\n\nA KType can have a webform schema - for e.g. data properties or a process schema for clustering KItems into a context.\n\nThe webform and process schema of a ktype may look as follows:" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "ktype = {\n", - " \"id\": \"characterization-process\",\n", - " \"name\": \"Characterization Process\",\n", - " \"webform_schema_id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n", - " \"webform_schema\": {\n", - " \"id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n", - " \"name\": \"Characterization Process\",\n", - " \"spec\": {\n", - " \"semantics_enabled\": True,\n", - " \"sections_enabled\": False,\n", - " \"class_mapping\": [\n", - " \"https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\"\n", - " ],\n", - " \"sections\": [\n", - " {\n", - " \"id\": \"id57f43923129f28\",\n", - " \"name\": \"Untitled Section\",\n", - " \"inputs\": [\n", - " {\n", - " \"id\": \"id02760f0b0cd56\",\n", - " \"label\": \"Start time\",\n", - " \"widget\": \"Text\",\n", - " \"relation_mapping\": {\n", - " \"iri\": \"http://www.w3.org/ns/dcat#startDate\",\n", - " \"label\": \"start date\",\n", - " \"type\": \"data_property\",\n", - " },\n", - " },\n", - " {\n", - " \"id\": \"id3ff5961015588\",\n", - " \"label\": \"End time \",\n", - " \"widget\": \"Text\",\n", - " \n", - " \"relation_mapping\": {\n", - " \"iri\": \"http://www.w3.org/ns/dcat#endDate\",\n", - " \"label\": \"end date\",\n", - " \"type\": \"data_property\",\n", - " },\n", - " }\n", - " ],\n", - " }\n", - " ]\n", - " },\n", - " },\n", - " \"process_schema_id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n", - " \"process_schema\": {\n", - " \"id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n", - " \"name\": \"Characterization Process\",\n", - " \"spec\": [\n", - " {\n", - " \"id\": \"specimen\",\n", - " \"label\": \"Specimen\",\n", - " },\n", - " {\n", - " \"id\": \"testingmachine\",\n", - " \"label\": \"TestingMachine\",\n", - " },\n", - " {\n", - " \"id\": \"expert\",\n", - " \"label\": \"expert\",\n", - " \"mappings\": [\n", - " {\n", - " \"dst_ktype_id\": \"organization\",\n", - " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", - " \"relation_name\": \"wasAssociatedWith\"\n", - " },\n", - " {\n", - " \"dst_ktype_id\": \"testingmachine\",\n", - " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", - " \"relation_name\": \"wasAssociatedWith\"\n", - " },\n", - " {\n", - " \"dst_ktype_id\": \"specimen\",\n", - " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", - " \"relation_name\": \"wasAssociatedWith\"\n", - " }\n", - " ],\n", - " },\n", - " {\n", - " \"id\": \"organization\",\n", - " \"label\": \"organization\",\n", - " }\n", - " ],\n", - " },\n", - "}" + "ktype = {\n \"id\": \"sdk-tutorial\",\n \"name\": \"SDK Tutorial\",\n \"webform_schema_id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n \"webform_schema\": {\n \"id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n \"name\": \"SDK Tutorial\",\n \"spec\": {\n \"semantics_enabled\": True,\n \"sections_enabled\": False,\n \"class_mapping\": [\n \"https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\"\n ],\n \"sections\": [\n {\n \"id\": \"id57f43923129f28\",\n \"name\": \"Untitled Section\",\n \"inputs\": [\n {\n \"id\": \"id02760f0b0cd56\",\n \"label\": \"Start time\",\n \"widget\": \"Text\",\n \"relation_mapping\": {\n \"iri\": \"http://www.w3.org/ns/dcat#startDate\",\n \"label\": \"start date\",\n \"type\": \"data_property\",\n },\n },\n {\n \"id\": \"id3ff5961015588\",\n \"label\": \"End time\",\n \"widget\": \"Text\",\n \n \"relation_mapping\": {\n \"iri\": \"http://www.w3.org/ns/dcat#endDate\",\n \"label\": \"end date\",\n \"type\": \"data_property\",\n },\n }\n ],\n }\n ]\n },\n },\n \"process_schema_id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n \"process_schema\": {\n \"id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n \"name\": \"SDK Tutorial\",\n \"spec\": [\n {\n \"id\": \"specimen\",\n \"label\": \"Specimen\",\n },\n {\n \"id\": \"testingmachine\",\n \"label\": \"TestingMachine\",\n },\n {\n \"id\": \"expert\",\n \"label\": \"expert\",\n \"mappings\": [\n {\n \"dst_ktype_id\": \"organization\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n },\n {\n \"dst_ktype_id\": \"testingmachine\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n },\n {\n \"dst_ktype_id\": \"specimen\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n }\n ],\n },\n {\n \"id\": \"organization\",\n \"label\": \"organization\",\n }\n ],\n },\n}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can see, that the ktype is describing a characterization process.\n", - "The webfrom describes two fields:\n", - "* start date of the characterization (with dcat mapping)\n", - "* end data of the characterization (with dcat mapping)\n", - "\n", - "The process schema describes the following entities:\n", - "\n", - "* the expert involed\n", - "* the testing machine involved\n", - "* the specimen involved\n", - "* the organization involed\n", - "* the semantic relations between the expert and the organization/specimen/testing machine" + "We can see, that the ktype is describing a SDK tutorial KType.\nThe webform describes two fields:\n* start date of the characterization (with dcat mapping)\n* end date of the characterization (with dcat mapping)\n\nThe process schema describes the following entities:\n\n* the expert involved\n* the testing machine involved\n* the specimen involved\n* the organization involved\n* the semantic relations between the expert and the organization/specimen/testing machine" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The Ktype can be instanciated with simple dictionary expansion:" + "The KType can be instantiated with simple dictionary expansion:" ] }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ktype:\n", - " id: characterization-process\n", - " name: Characterization Process\n", - " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " webform_schema:\n", - " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " name: Characterization Process\n", - " spec:\n", - " semantics_enabled: true\n", - " sections_enabled: false\n", - " class_mapping:\n", - " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", - " sections:\n", - " - id: id57f43923129f28\n", - " name: Untitled Section\n", - " inputs:\n", - " - id: id02760f0b0cd56\n", - " label: Start time\n", - " widget: Text\n", - " relation_mapping:\n", - " iri: http://www.w3.org/ns/dcat#startDate\n", - " label: start date\n", - " type: data_property\n", - " - id: id3ff5961015588\n", - " label: 'End time '\n", - " widget: Text\n", - " relation_mapping:\n", - " iri: http://www.w3.org/ns/dcat#endDate\n", - " label: end date\n", - " type: data_property\n", - " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", - " process_schema:\n", - " id: ee815110-ee41-44cf-a049-5873942440d6\n", - " name: Characterization Process\n", - " spec:\n", - " - id: specimen\n", - " label: Specimen\n", - " - id: testingmachine\n", - " label: TestingMachine\n", - " - id: expert\n", - " label: expert\n", - " mappings:\n", - " - dst_ktype_id: organization\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " - dst_ktype_id: testingmachine\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " - dst_ktype_id: specimen\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " - id: organization\n", - " label: organization" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "ktype = KType(**ktype)\n", "\n", @@ -255,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -272,126 +114,9 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/site-packages/pydantic/main.py:463: UserWarning: Pydantic serializer warnings:\n", - " PydanticSerializationUnexpectedValue(Expected `WebformSchema` - serialized value may not be as expected [input_value={'id': '69e58442-5837-4bb...-08-13T15:19:53.355557'}, input_type=dict])\n", - " PydanticSerializationUnexpectedValue(Expected `ProcessSchema` - serialized value may not be as expected [input_value={'id': 'ee815110-ee41-44c...-08-13T15:16:24.873119'}, input_type=dict])\n", - " return self.__pydantic_serializer__.to_python(\n" - ] - }, - { - "data": { - "text/plain": [ - "ktype:\n", - " id: characterization-process\n", - " name: Characterization Process\n", - " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " webform_schema:\n", - " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " name: Characterization Process\n", - " spec:\n", - " semanticsEnabled: true\n", - " sectionsEnabled: false\n", - " classMapping:\n", - " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", - " sections:\n", - " - id: id57f43923129f28\n", - " name: Untitled Section\n", - " inputs:\n", - " - id: id02760f0b0cd56\n", - " label: Start time\n", - " widget: Text\n", - " required: false\n", - " value: null\n", - " hint: null\n", - " hidden: false\n", - " ignore: false\n", - " selectOptions: []\n", - " measurementUnit: null\n", - " relationMapping:\n", - " iri: http://www.w3.org/ns/dcat#startDate\n", - " label: start date\n", - " type: data_property\n", - " classIri: null\n", - " relationMappingExtra: null\n", - " multipleSelection: false\n", - " knowledgeType: null\n", - " rangeOptions: null\n", - " placeholder: null\n", - " - id: id3ff5961015588\n", - " label: 'End time '\n", - " widget: Text\n", - " required: false\n", - " value: null\n", - " hint: null\n", - " hidden: false\n", - " ignore: false\n", - " selectOptions: []\n", - " measurementUnit: null\n", - " relationMapping:\n", - " iri: http://www.w3.org/ns/dcat#endDate\n", - " label: end date\n", - " type: data_property\n", - " classIri: null\n", - " relationMappingExtra: null\n", - " multipleSelection: false\n", - " knowledgeType: null\n", - " rangeOptions: null\n", - " placeholder: null\n", - " hidden: false\n", - " created_at: '2025-08-13T15:16:24.730491'\n", - " updated_at: '2025-08-13T15:19:53.355557'\n", - " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", - " process_schema:\n", - " id: ee815110-ee41-44cf-a049-5873942440d6\n", - " name: Characterization Process\n", - " spec:\n", - " - id: specimen\n", - " label: Specimen\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " - id: testingmachine\n", - " label: TestingMachine\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " - id: expert\n", - " label: expert\n", - " isChild: false\n", - " mappings:\n", - " - dstKtypeId: organization\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " - dstKtypeId: testingmachine\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " - dstKtypeId: specimen\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " children: []\n", - " - id: organization\n", - " label: organization\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " created_at: '2025-08-13T15:16:24.873119'\n", - " updated_at: '2025-08-13T15:16:24.873119'\n", - " created_at: '2025-08-13T15:34:31.201502'\n", - " updated_at: '2025-08-13T15:34:31.268380'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "ktype" ] @@ -412,103 +137,11 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ktype:\n", - " id: characterization-process\n", - " name: Characterization Process\n", - " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " webform_schema:\n", - " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " name: Characterization Process\n", - " spec:\n", - " semantics_enabled: true\n", - " sections_enabled: false\n", - " class_mapping:\n", - " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", - " sections:\n", - " - id: id57f43923129f28\n", - " name: Untitled Section\n", - " inputs:\n", - " - id: id02760f0b0cd56\n", - " label: Start time\n", - " widget: Text\n", - " required: false\n", - " hidden: false\n", - " ignore: false\n", - " select_options: []\n", - " relation_mapping:\n", - " iri: http://www.w3.org/ns/dcat#startDate\n", - " label: start date\n", - " type: data_property\n", - " multiple_selection: false\n", - " - id: id3ff5961015588\n", - " label: 'End time '\n", - " widget: Text\n", - " required: false\n", - " hidden: false\n", - " ignore: false\n", - " select_options: []\n", - " relation_mapping:\n", - " iri: http://www.w3.org/ns/dcat#endDate\n", - " label: end date\n", - " type: data_property\n", - " multiple_selection: false\n", - " hidden: false\n", - " created_at: '2025-08-13T15:16:24.730491'\n", - " updated_at: '2025-08-13T15:19:53.355557'\n", - " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", - " process_schema:\n", - " id: ee815110-ee41-44cf-a049-5873942440d6\n", - " name: Characterization Process\n", - " spec:\n", - " - id: specimen\n", - " label: Specimen\n", - " is_child: false\n", - " mappings: []\n", - " children: []\n", - " - id: testingmachine\n", - " label: TestingMachine\n", - " is_child: false\n", - " mappings: []\n", - " children: []\n", - " - id: expert\n", - " label: expert\n", - " is_child: false\n", - " mappings:\n", - " - dst_ktype_id: organization\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " - dst_ktype_id: testingmachine\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " - dst_ktype_id: specimen\n", - " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relation_name: wasAssociatedWith\n", - " children: []\n", - " - id: organization\n", - " label: organization\n", - " is_child: false\n", - " mappings: []\n", - " children: []\n", - " created_at: 2025-08-13 15:16:24.873119\n", - " updated_at: 2025-08-13 15:16:24.873119\n", - " created_at: '2025-08-13T15:34:31.201502'\n", - " updated_at: '2025-08-13T15:34:31.268380'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ktype = dsms.ktypes.CharacterizationProcess\n", - "ktype" + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ktype = dsms.ktypes.SdkTutorial\nktype" ] }, { @@ -520,123 +153,11 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ktype:\n", - " id: characterization-process\n", - " name: Characterization Procedure\n", - " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " webform_schema:\n", - " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", - " name: Characterization Process\n", - " spec:\n", - " semanticsEnabled: true\n", - " sectionsEnabled: false\n", - " classMapping:\n", - " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", - " sections:\n", - " - id: id57f43923129f28\n", - " name: Untitled Section\n", - " inputs:\n", - " - id: id02760f0b0cd56\n", - " label: Start time\n", - " widget: Text\n", - " required: false\n", - " value: null\n", - " hint: null\n", - " hidden: false\n", - " ignore: false\n", - " selectOptions: []\n", - " measurementUnit: null\n", - " relationMapping:\n", - " iri: http://www.w3.org/ns/dcat#startDate\n", - " label: start date\n", - " type: data_property\n", - " classIri: null\n", - " relationMappingExtra: null\n", - " multipleSelection: false\n", - " knowledgeType: null\n", - " rangeOptions: null\n", - " placeholder: null\n", - " - id: id3ff5961015588\n", - " label: 'End time '\n", - " widget: Text\n", - " required: false\n", - " value: null\n", - " hint: null\n", - " hidden: false\n", - " ignore: false\n", - " selectOptions: []\n", - " measurementUnit: null\n", - " relationMapping:\n", - " iri: http://www.w3.org/ns/dcat#endDate\n", - " label: end date\n", - " type: data_property\n", - " classIri: null\n", - " relationMappingExtra: null\n", - " multipleSelection: false\n", - " knowledgeType: null\n", - " rangeOptions: null\n", - " placeholder: null\n", - " hidden: false\n", - " created_at: '2025-08-13T15:16:24.730491'\n", - " updated_at: '2025-08-13T15:19:53.355557'\n", - " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", - " process_schema:\n", - " id: ee815110-ee41-44cf-a049-5873942440d6\n", - " name: Characterization Process\n", - " spec:\n", - " - id: specimen\n", - " label: Specimen\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " - id: testingmachine\n", - " label: TestingMachine\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " - id: expert\n", - " label: expert\n", - " isChild: false\n", - " mappings:\n", - " - dstKtypeId: organization\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " - dstKtypeId: testingmachine\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " - dstKtypeId: specimen\n", - " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", - " relationName: wasAssociatedWith\n", - " children: []\n", - " - id: organization\n", - " label: organization\n", - " isChild: false\n", - " mappings: []\n", - " children: []\n", - " created_at: '2025-08-13T15:16:24.873119'\n", - " updated_at: '2025-08-13T15:16:24.873119'\n", - " created_at: '2025-08-13T15:34:31.201502'\n", - " updated_at: '2025-08-13T15:34:31.904250'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ktype.name = 'Characterization Procedure'\n", - "\n", - "dsms.add(ktype)\n", - "dsms.commit()\n", - "\n", - "ktype" + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ktype.name = 'SDK Tutorial Updated'\n\ndsms.add(ktype)\ndsms.commit()\n\nktype" ] }, { @@ -655,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -671,19 +192,131 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dsms.commit()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7.5. KType v2: Semantic Specifications\n\nKType v2 extends the base `KType` with a semantic specification (`KTypeSpec`) that captures ontology classes, relations, schema references, inheritance, and versioning. The v2 endpoints are available via `DSMS.get_v2_ktypes()` and related methods." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.1. Listing v2 KTypes\n\n`DSMS.get_v2_ktypes()` returns a list of `KTypeV2` objects. Each has an optional `spec` attribute of type `KTypeSpec`. KTypes that were created through the legacy v1 endpoint have `spec=None`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dsms.knowledge.ktype import KTypeV2\n\nv2_ktypes = dsms.get_v2_ktypes()\nfor kt in v2_ktypes:\n print(kt.id, \"| spec:\", kt.spec is not None)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.2. Creating a v2 KType\n\nUse `CreateKTypeRequest` to define the new KType. The `id` must be a lowercase slug (letters, digits, and hyphens only, starting with a letter)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dsms.knowledge.ktype import CreateKTypeRequest, OntologyClassSpec\n\nrequest = CreateKTypeRequest(\n id=\"tutorial-material\",\n name=\"Tutorial Material\",\n version=\"1.0.0\",\n description=\"A KType created in the tutorial.\",\n ontology_classes=[\n OntologyClassSpec(\n iri=\"https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\",\n label=\"Material\",\n ontology=\"https://emmo.info/emmo\",\n )\n ],\n tags=[\"tutorial\", \"material\"],\n)\n\nv2_ktype = dsms.create_v2_ktype(request)\nprint(v2_ktype)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.3. Updating a v2 KType specification\n\nUse `KTypeSpecPayload` for partial updates. Only the fields you set will be changed on the server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dsms.knowledge.ktype import KTypeSpecPayload\n\npayload = KTypeSpecPayload(\n description=\"An updated description for the tutorial material KType.\",\n tags=[\"tutorial\", \"material\", \"updated\"],\n)\n\nv2_ktype = dsms.update_v2_ktype(\"tutorial-material\", payload)\nprint(v2_ktype)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.4. Importing a v2 KType from a remote URL\n\nKType specifications hosted in a remote repository (for example a GitHub repository) can be imported directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# v2_ktype = dsms.import_v2_ktype(\"https://raw.githubusercontent.com/your-org/your-repo/main/ktypes/my-ktype.yaml\")\n# print(v2_ktype)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.5. Checking for remote updates\n\n`DSMS.get_v2_ktype_remote_diff()` compares the local KType spec against the latest version in the remote repository and reports which fields have changed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# diff = dsms.get_v2_ktype_remote_diff(\"tutorial-material\")\n# print(\"Identical:\", diff.identical)\n# for changed in diff.changed_fields:\n# print(changed.field, \"local:\", changed.local, \"remote:\", changed.remote)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.5.6. Deleting a v2 KType\n\nDeletion is blocked if KItems of that type still exist. Delete all KItems of the type first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dsms.delete_v2_ktype(\"tutorial-material\")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "The available KTypes in the SDK can be fetched from an enum list." ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# List all available KTypes as an enum\ndsms.ktypes" + ] } ], "metadata": { diff --git a/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb b/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb index d8d12dd..b43e4a5 100644 --- a/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb +++ b/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb @@ -4,25 +4,17 @@ "cell_type": "markdown", "id": "08e6e825", "metadata": {}, - "source": [ - "# 8. Adding Kitems into context" - ] + "source": "# 8. KItem Contexts" }, { "cell_type": "markdown", "id": "01266cd0", "metadata": {}, - "source": [ - "## 8.1. Setting up\n", - "\n", - "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, alongwith with installation of this package and have establised access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", - "\n", - "Import the needed classes and functions." - ] + "source": "## 8.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nImport the needed classes and functions." }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "8e119a92", "metadata": {}, "outputs": [], @@ -40,21 +32,20 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "ddc6cdfa", "metadata": {}, "outputs": [], "source": [ - "dsms = DSMS(env=\".env\")" + "import os\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" ] }, { "cell_type": "markdown", "id": "7206cca4", "metadata": {}, - "source": [ - "## 8.2 Putting KItems into contexts" - ] + "source": "## 8.2. Putting KItems into contexts" }, { "cell_type": "markdown", @@ -69,167 +60,166 @@ "id": "ed1d8179", "metadata": {}, "source": [ - "We will quickly create a KItem of type dataset and we will put it into context with a dataset catalog." + "A KItem that is designated as a context (e.g. a `Project`) can group other KItems. A KItem joins a context by setting its `contexts` field to include the context KItem.\n\nWe will create a `Project` as the context and a `Dataset` as its member." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "b7dcb02f", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:406: UserWarning: A flat dictionary was provided for custom properties.\n", - " Will be transformed into `KItemCustomPropertiesModel`.\n", - " warnings.warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://bue.materials-data.space/knowledge/dataset/testdataset-edc21685\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Tensile Strength [MPa]`. Cannot check if value is of correct type.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Material`. Cannot check if value is of correct type.\n", - " warnings.warn(\n" - ] - } - ], + "outputs": [], "source": [ - "dataset = KItem(\n", - " name=\"Test dataset\",\n", - " ktype_id=dsms.ktypes.Dataset,\n", - " custom_properties={\n", - " \"Tensile Strength [MPa]\": 180,\n", - " \"Material\": \"DXD100\"\n", - " }\n", - " )\n", + "# Project is a context-capable KType (its spec has context=True)\n", + "project = KItem(\n", + " name=\"Test project\",\n", + " ktype_id=dsms.ktypes.Project,\n", + ")\n", "\n", - "dsms.add(dataset)\n", + "dsms.add(project)\n", "dsms.commit()\n", "\n", - "print(dataset.url)" + "print(project.url)" ] }, + { + "cell_type": "markdown", + "id": "7fb7d96c", + "source": "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control.", + "metadata": {} + }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "c7a41db1", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/site-packages/pydantic/main.py:463: UserWarning: Pydantic serializer warnings:\n", - " PydanticSerializationUnexpectedValue(Expected `str` - serialized value may not be as expected [input_value=False, input_type=bool])\n", - " return self.__pydantic_serializer__.to_python(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://bue.materials-data.space/knowledge/dataset-catalog/testcatalog-4bcfabbb\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Project`. Cannot check if value is of correct type.\n", - " warnings.warn(\n" - ] - } - ], + "outputs": [], "source": [ - "catalog = KItem(\n", - " name=\"Test catalog\",\n", - " ktype_id=dsms.ktypes.DatasetCatalog,\n", - " custom_properties={\"Project\": \"Mechanical testing campaign 1\"},\n", - " contexts=[dataset]\n", + "# Assign the dataset to the project context via the contexts field\n", + "dataset = KItem(\n", + " name=\"Test dataset\",\n", + " ktype_id=dsms.ktypes.Dataset,\n", + " contexts=[project],\n", ")\n", "\n", - "dsms.add(catalog)\n", + "dsms.add(dataset)\n", "dsms.commit()\n", "\n", - "print(catalog.url)" + "print(dataset.url)" ] }, + { + "cell_type": "markdown", + "id": "db37aaad", + "source": [ + "We can verify the context relationship from both directions: checking that the dataset reports its context, and checking that a freshly-fetched dataset shows `has_contexts=True`." + ], + "metadata": {} + }, + { + "cell_type": "code", + "id": "df636cb5", + "source": [ + "# From the dataset side: which contexts does this dataset belong to?\n", + "print(\"Dataset contexts:\", dataset.contexts)\n", + "\n", + "# Verify via a fresh fetch\n", + "refreshed_dataset = dsms[str(dataset.id)]\n", + "print(\"Dataset has_contexts:\", refreshed_dataset.has_contexts)" + ], + "metadata": {}, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "id": "20026edb", "metadata": {}, "source": [ - "Now we can inspect the catalog and see which datasets are in context:" + "Now we can inspect the project:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "640813be", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "kitem:\n", - " id: 4bcfabbb-dabf-4d4f-a964-2d56db7cdf54\n", - " name: Test catalog\n", - " ktype_id: dataset-catalog\n", - " slug: testcatalog-4bcfabbb\n", - " annotations: []\n", - " attachments: []\n", - " linked_kitems: []\n", - " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", - " contacts: []\n", - " created_at: 2025-08-13 13:03:06.234294\n", - " updated_at: 2025-08-13 13:03:06.234294\n", - " external_links: []\n", - " apps: []\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id1755090185982jzxbpc\n", - " name: Misc\n", - " entries:\n", - " - id: id17550901859828l3uet\n", - " type: Text\n", - " label: Project\n", - " value: Mechanical testing campaign 1\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: false\n", - " contexts:\n", - " - id: edc21685-2d25-4d7a-bd2e-7c757371d3f8\n", - " name: Test dataset\n", - " ktype_id: dataset\n", - " slug: testdataset-edc21685" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "catalog" + "project" ] + }, + { + "cell_type": "markdown", + "id": "d59e0f61", + "source": "## 8.3. Searching within a context\n\n`DSMS.search()` accepts a `contexts` parameter: a list of KItem IDs. The search returns only KItems that belong to at least one of the specified contexts.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "e6b97a13", + "source": [ + "# Search for KItems inside the project context\n", + "results = dsms.search(contexts=[str(project.id)])\n", + "for r in results:\n", + " print(r.kitem.name, \"| has_contexts:\", r.kitem.has_contexts)" + ], + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "1c020217", + "source": "The `has_contexts` field on `KItemCompactedModel` is a boolean that indicates whether the KItem belongs to at least one context. It is populated by the server and available on search results without needing to fetch the full KItem.", + "metadata": {} + }, + { + "cell_type": "markdown", + "id": "32f71fd1", + "source": "## 8.4. Context-scoped SPARQL queries\n\nTwo SPARQL methods on `SparqlInterface` operate within the scope of a context KItem rather than the full triplestore.\n\n`sparql_interface.query_context(context_id, query)` sends a standard SPARQL SELECT query scoped to the given context and returns a JSON result object (same format as `sparql_interface.query()`).\n\n`sparql_interface.graph_context(context_id, query)` sends a graph query scoped to the context and returns a JSON graph result.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "875b69d5", + "source": [ + "sparql_query = \"\"\"\n", + "SELECT ?s ?p ?o\n", + "WHERE {\n", + " ?s ?p ?o .\n", + "}\n", + "LIMIT 10\n", + "\"\"\"\n", + "\n", + "try:\n", + " results = dsms.sparql_interface.query_context(str(project.id), sparql_query)\n", + " print(results)\n", + "except RuntimeError as e:\n", + " print(f\"Note: context SPARQL requires RDF knowledge graphs for context members.\")\n", + " print(f\"Error: {e}\")" + ], + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "6d37b2bf", + "source": "## 8.5. Cleanup\n\nDelete the KItems created during this tutorial.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "6f764bfd", + "source": [ + "del dsms[dataset]\n", + "del dsms[project]\n", + "dsms.commit()" + ], + "metadata": {}, + "execution_count": null, + "outputs": [] } ], "metadata": { diff --git a/docs/index.md b/docs/index.md index 5cd203a..bbacaab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,6 +60,7 @@ The Data Space Management System dsms_sdk/dsms_sdk dsms_sdk/dsms_config_schema dsms_sdk/dsms_kitem_schema +release-checklist ``` diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 0000000..94d4a2c --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,215 @@ +# Release Checklist + +Work through this list in order when preparing a new SDK release. Each section states which files to touch and what to verify. + +Commands assume your working directory is the **repository root** and your virtual environment is active: + +```bash +python -m venv .venv && source .venv/bin/activate +pip install -e ".[docs,tests,pre_commit]" +``` + +--- + +## 0. Classify the change + +Determine the semantic-versioning impact before touching any file: + +| Change type | Version bump | Examples | +|---|---|---| +| Typo, doc fix, internal refactor with no API change | **patch** (x.y.Z) | Fix docstring, rename internal variable | +| New field, method, or parameter (backward-compatible) | **minor** (x.Y.0) | Add `schema_data` field to `KItem` | +| Removed or renamed public API, breaking model change | **major** (X.0.0) | Rename `user_groups` to `access_properties`, drop Python version | + +--- + +## 1. `setup.cfg` + +- [ ] Bump `version` to the new version string (format: `vMAJOR.MINOR.PATCH`). +- [ ] Update `python_requires` if the minimum Python version changed. +- [ ] Add, remove, or relax dependency pins in `install_requires` if needed. + +**Verify:** + +```bash +python -c "import configparser; c = configparser.ConfigParser(); c.read('setup.cfg'); print(c['metadata']['version'])" +``` + +--- + +## 2. `CHANGELOG.md` + +- [ ] Add a new version section **above** the previous entry: + + ```markdown + ## [x.y.z] — YYYY-MM-DD + + ### Added + - ... + + ### Changed + - ... + + ### Deprecated + - ... + + ### Removed + - ... + + ### Fixed + - ... + ``` + +- [ ] Use today's date in `YYYY-MM-DD` format. +- [ ] Cover every public API change, new model, and deprecation. + +--- + +## 3. `README.md` + +- [ ] Update the compatibility table: add a row for the new SDK version and its required DSMS backend version. +- [ ] Update the Usage capabilities list if new top-level features were added. + +--- + +## 4. `docs/dsms_sdk/dsms_sdk.md` + +- [ ] Update the compatibility table to match `README.md`. +- [ ] Update the numbered capabilities list if the feature set changed. + +--- + +## 5. `docs/dsms_sdk/dsms_kitem_schema.md` + +Update this file when any of the following change: + +- [ ] New or removed fields on `KItem` or `KItemCompactedModel`: update the field tables. +- [ ] New property sub-models (e.g. `KItemAccessProperties`, `KItemSchemaData`): add a dedicated section with field table and example. +- [ ] New or removed `Widget` enum values: update the Widget Fields table. +- [ ] Deprecated fields: mark them clearly in the table and add a deprecation note. + +--- + +## 6. `docs/dsms_sdk/dsms_config_schema.md` + +- [ ] Update the Configuration Fields table if any `Configuration` fields were added, removed, or changed. + +--- + +## 7. Tutorial notebooks + +Update notebooks when any of the following change: + +- [ ] Public API signatures (`DSMS.search()`, `DSMS.get_kitems()`, `KItem` fields, etc.) +- [ ] New features worth demonstrating (new search filters, new KType v2 methods, new property models) +- [ ] Deprecated fields used in notebook code cells + +Work cell by cell. Do **not** hand-edit output cells; regenerate them with the refresh script (see step 8). + +Notebooks live in `docs/dsms_sdk/tutorials/`. Specific guidance: + +| Notebook | When to update | +|---|---| +| `1_introduction.ipynb` | KItem/KType field changes, new top-level SDK features | +| `2_creation.ipynb` | New KItem fields, new `access_properties` / `schema_data` patterns | +| `3_updating.ipynb` | Changed update workflows, new updatable fields | +| `4_deletion.ipynb` | Changed deletion behaviour | +| `5_search.ipynb` | New `DSMS.search()` parameters or filters | +| `6_apps.ipynb` | App config or pipeline changes | +| `7_ktypes.ipynb` | KType v1 or v2 API changes | +| `8_kitem_contexts.ipynb` | Context model or context-scoped SPARQL changes | + +--- + +## 8. Run and refresh notebooks + +> **Requirement:** a live DSMS instance must be reachable. Set credentials in a `.env` file or via environment variables (`DSMS_HOST_URL`, `DSMS_USERNAME`, `DSMS_PASSWORD` or `DSMS_TOKEN`) before running. + +**Test all notebooks without saving outputs (CI-safe):** + +```bash +./scripts/run_notebooks.sh +``` + +**Re-execute and save outputs in-place (for documentation commits):** + +```bash +./scripts/run_notebooks.sh --refresh +``` + +**Refresh a single notebook:** + +```bash +./scripts/run_notebooks.sh --refresh docs/dsms_sdk/tutorials/7_ktypes.ipynb +``` + +Inspect the saved outputs before committing: + +- Every code cell must have output (no silent failures). +- No cell output should contain a Python traceback. +- Deprecated-field warnings (if any) should appear in the expected cells only. + +--- + +## 9. Pre-commit hooks + +Run all linters and formatters across every changed Python file: + +```bash +pre-commit run --files ... +``` + +All hooks must pass before committing. Do not use `--no-verify`. + +--- + +## 10. Commit and tag + +- [ ] Stage files by name; do not use `git add .` or `git add -A`. +- [ ] Write a concise commit message: `release: bump to vX.Y.Z` +- [ ] Tag the release commit: + + ```bash + git tag vX.Y.Z + git push origin vX.Y.Z + ``` + +- [ ] Verify the tag is visible on the remote: + + ```bash + git ls-remote --tags origin | grep vX.Y.Z + ``` + +--- + +## 11. PyPI publish + +- [ ] Build the distribution: + + ```bash + python -m build + ``` + +- [ ] Upload to PyPI: + + ```bash + twine upload dist/dsms_sdk-X.Y.Z* + ``` + +- [ ] Verify the new version is visible at https://pypi.org/project/dsms-sdk/. + +--- + +## Quick reference + +| File | patch | minor | major | +|---|:---:|:---:|:---:| +| `setup.cfg` (version) | yes | yes | yes | +| `CHANGELOG.md` | yes | yes | yes | +| `README.md` (compat table) | no | yes | yes | +| `README.md` (capabilities) | no | maybe | yes | +| `docs/dsms_sdk/dsms_sdk.md` | no | maybe | yes | +| `docs/dsms_sdk/dsms_kitem_schema.md` | no | yes | yes | +| `docs/dsms_sdk/dsms_config_schema.md` | no | maybe | yes | +| Tutorial notebooks | no | maybe | yes | +| Run `scripts/run_notebooks.sh` | no | yes | yes | diff --git a/scripts/run_notebooks.sh b/scripts/run_notebooks.sh new file mode 100755 index 0000000..ea17893 --- /dev/null +++ b/scripts/run_notebooks.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# run_notebooks.sh: execute and validate all tutorial notebooks +# +# Usage: +# ./scripts/run_notebooks.sh # test all notebooks +# ./scripts/run_notebooks.sh --refresh # re-execute and save outputs in-place +# ./scripts/run_notebooks.sh path/to/nb.ipynb # test a single notebook +# ./scripts/run_notebooks.sh --refresh path/to/nb.ipynb # refresh a single notebook +# +# Requirements: activate your virtual environment first. +# python -m venv .venv && source .venv/bin/activate +# pip install -e ".[docs,tests]" +# +# Environment: the notebooks connect to a live DSMS instance. +# Set credentials in a .env file or via environment variables before running: +# DSMS_HOST_URL, DSMS_USERNAME, DSMS_PASSWORD (or DSMS_TOKEN) + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TUTORIALS_DIR="$REPO_ROOT/docs/dsms_sdk/tutorials" +cd "$REPO_ROOT" + +# Collect notebooks +if [ $# -ge 1 ] && [ "$1" = "--refresh" ]; then + MODE="refresh" + if [ $# -ge 2 ]; then + NOTEBOOKS=("${@:2}") + else + mapfile -t NOTEBOOKS < <( + find "$TUTORIALS_DIR" -name "*.ipynb" \ + ! -path "*/.ipynb_checkpoints/*" \ + | sort + ) + fi +elif [ $# -ge 1 ]; then + NOTEBOOKS=("$@") + MODE="test" +else + mapfile -t NOTEBOOKS < <( + find "$TUTORIALS_DIR" -name "*.ipynb" \ + ! -path "*/.ipynb_checkpoints/*" \ + | sort + ) + MODE="test" +fi + +# Export .env variables so notebook kernels can connect without a local .env file +if [ -f "$REPO_ROOT/.env" ]; then + set -a + # shellcheck source=/dev/null + source "$REPO_ROOT/.env" + set +a +fi + +echo "Found ${#NOTEBOOKS[@]} notebook(s)." +echo "" + +if [ "$MODE" = "refresh" ]; then + echo "Mode: refresh (executing and saving outputs in-place)" + echo "" + for nb in "${NOTEBOOKS[@]}"; do + echo " Refreshing: $nb" + jupyter nbconvert \ + --to notebook \ + --execute \ + --inplace \ + --ExecutePreprocessor.timeout=300 \ + "$nb" + done + echo "" + echo "Done. Commit the updated notebooks together with any documentation changes." +else + echo "Mode: test (pytest + nbmake, outputs not saved)" + echo "" + pytest --nbmake "${NOTEBOOKS[@]}" +fi diff --git a/setup.cfg b/setup.cfg index c21f92f..a13db52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,7 @@ pre_commit = tests = pytest>=7.4.3 pytest-mock + pytest-nbmake responses [bumpver] From a14d5402a5537f707a5e808d5dd2a18ecf3c8009 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Sun, 7 Jun 2026 16:28:30 -0400 Subject: [PATCH 36/48] Rename special groups and remove ADMIN role; add visibility to access properties - Rename INTERNALLY_PUBLIC_GROUP/EXTERNALLY_PUBLIC_GROUP to INTERNAL_GROUP/PUBLIC_GROUP - Rename config fields: id_internally_public/id_externally_public/label_internally_public/label_externally_public to id_internal/id_public/label_internal/label_public - Default group IDs changed from dsms:internally-public / dsms:externally-public to dsms:internal / dsms:public - Remove Role.ADMIN (max role is now OWNER); roles serialize as lowercase strings on the wire - Add visibility field to KItemAccessProperties (private/internal/public) - Update tests and docs to match --- CHANGELOG.md | 4 +- README.md | 2 +- docs/dsms_sdk/dsms_kitem_schema.md | 2 - docs/dsms_sdk/dsms_sdk.md | 2 +- docs/dsms_sdk/tutorials/2_creation.ipynb | 6 +-- dsms/core/configuration.py | 24 ++++++------ dsms/knowledge/groups/__init__.py | 8 ++-- dsms/knowledge/groups/public.py | 18 ++++----- dsms/knowledge/properties/access.py | 36 +++++++++++------- tests/test_access.py | 41 +++++++-------------- tests/test_access_extended.py | 47 ++++++++++++------------ tests/test_groups.py | 44 +++++++++++----------- 12 files changed, 111 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b01bf7..5c856c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,13 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). **Access control (RBAC)** - `KItemAccessProperties` model with `user_access` and `group_access` lists for per-KItem role assignments. -- `Role` enum (`MEMBER=1`, `CONTRIBUTOR=2`, `OWNER=3`, `ADMIN=4`) and `OperationType` enum (`create`, `read`, `update`, `delete`, `manage`). +- `Role` enum (`MEMBER=1`, `CONTRIBUTOR=2`, `OWNER=3`) and `OperationType` enum (`create`, `read`, `update`, `delete`, `manage`). Roles serialize as lowercase strings on the wire (`"member"`, `"contributor"`, `"owner"`). - `RoleMapping` enum with `get_operations`, `min_access_level`, and `max_access_level` helpers. - `UserAccessProperty` and `GroupAccessProperty` sub-models. - `DSMS.user_groups` and `DSMS.users` cached properties with `refresh_user_groups()` / `refresh_users()` invalidation. - `DSMS.get_user(user_id)` convenience method. - `Group`, `User`, `GroupList`, `UserList` models (`dsms.knowledge.groups`). -- `INTERNALLY_PUBLIC_GROUP` / `EXTERNALLY_PUBLIC_GROUP` constants, configurable via environment variables. +- `INTERNAL_GROUP` / `PUBLIC_GROUP` constants (IDs `dsms:internal` / `dsms:public`), configurable via environment variables. - `refresh_public_groups(config)` to avoid import-time staleness when custom group IDs are used. **KType v2 semantic-spec subsystem** diff --git a/README.md b/README.md index edbd983..0951366 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The SDK provides a general Python interface to a remote DSMS deployment, allowin - Semantic annotation of KItems - Attaching semantic schema data (ontology-class instance data) to KItems - Managing Knowledge Types (KTypes), including the v2 semantic-spec subsystem for defining ontology classes, relations, and schema references -- Role-based access control (RBAC) per KItem: assign users and groups to `MEMBER`, `CONTRIBUTOR`, `OWNER`, or `ADMIN` roles +- Role-based access control (RBAC) per KItem: assign users and groups to `MEMBER`, `CONTRIBUTOR`, or `OWNER` roles - Conduct free-text searches within the DSMS instance with filters (KType, annotation, context membership, attachment extension) as well as a full SPARQL interface (including context-scoped queries) - Linking KItems to other KItems, and grouping them via context KItems - Linking Apps to KItems, triggered, for example, during a file upload diff --git a/docs/dsms_sdk/dsms_kitem_schema.md b/docs/dsms_sdk/dsms_kitem_schema.md index a2b7c0a..ac97b2c 100644 --- a/docs/dsms_sdk/dsms_kitem_schema.md +++ b/docs/dsms_sdk/dsms_kitem_schema.md @@ -301,8 +301,6 @@ sample_kitem.user_groups = [ | `MEMBER` | 1 | READ | | `CONTRIBUTOR` | 2 | READ, UPDATE | | `OWNER` | 3 | READ, UPDATE, DELETE, MANAGE | -| `ADMIN` | 4 | READ, UPDATE, DELETE, MANAGE | - ### KItemAccessProperties Sub-fields | Field Name | Description | Type | Default | Property Namespace | Required/Optional | diff --git a/docs/dsms_sdk/dsms_sdk.md b/docs/dsms_sdk/dsms_sdk.md index 77ec641..c0edc99 100644 --- a/docs/dsms_sdk/dsms_sdk.md +++ b/docs/dsms_sdk/dsms_sdk.md @@ -23,7 +23,7 @@ The SDK functionalities are listed below: 3. Semantic annotation of KItems. 4. Attaching semantic schema data (ontology-class instance data) to KItems. 5. Managing Knowledge Types (KTypes), including the v2 semantic-spec subsystem. -6. Role-based access control (RBAC) per KItem: assign users and groups to roles (`MEMBER`, `CONTRIBUTOR`, `OWNER`, `ADMIN`). +6. Role-based access control (RBAC) per KItem: assign users and groups to roles (`MEMBER`, `CONTRIBUTOR`, `OWNER`). 7. Free-text search with filters (KType, annotation, context membership, attachment extension) and a full SPARQL interface including context-scoped queries. 8. Linking KItems to other KItems, and grouping them via context KItems. 9. Linking Apps to KItems, triggered, for example, during a file upload. diff --git a/docs/dsms_sdk/tutorials/2_creation.ipynb b/docs/dsms_sdk/tutorials/2_creation.ipynb index b672eda..eb9f035 100644 --- a/docs/dsms_sdk/tutorials/2_creation.ipynb +++ b/docs/dsms_sdk/tutorials/2_creation.ipynb @@ -237,11 +237,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "### 2.x. Setting access properties\n", - "\n", - "Access control is defined via `access_properties`, which assigns roles to specific users and groups. The available roles are `MEMBER` (read only), `CONTRIBUTOR` (read and update), `OWNER` (read, update, delete, manage), and `ADMIN` (same as OWNER)." - ] + "source": "### 2.x. Setting access properties\n\nAccess control is defined via `access_properties`, which assigns roles to specific users and groups. The available roles are `MEMBER` (read only), `CONTRIBUTOR` (read and update), and `OWNER` (read, update, delete, manage)." }, { "cell_type": "code", diff --git a/dsms/core/configuration.py b/dsms/core/configuration.py index 60f1d72..1f26f05 100644 --- a/dsms/core/configuration.py +++ b/dsms/core/configuration.py @@ -46,24 +46,24 @@ class Loglevel(Enum): class BaseConfiguration(BaseSettings): """Base Configuration for DSMS-SDK""" - label_internally_public: str = Field( - "Internally Public", - description="Label to use for KItems marked as `internally_public`.", + label_internal: str = Field( + "Internal", + description="Label to use for the internal visibility group.", ) - label_externally_public: str = Field( - "Externally Public", - description="Label to use for KItems marked as `externally_public`.", + label_public: str = Field( + "Public", + description="Label to use for the public visibility group.", ) - id_internally_public: str = Field( - "dsms:internally-public", - description="ID to use for KItems marked as `internally_public`.", + id_internal: str = Field( + "dsms:internal", + description="ID of the special group that grants read access to all authenticated users.", ) - id_externally_public: str = Field( - "dsms:externally-public", - description="ID to use for KItems marked as `externally_public`.", + id_public: str = Field( + "dsms:public", + description="ID of the special group that grants read access to all users.", ) model_config = ConfigDict(use_enum_values=True) diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index 05bf012..204b1af 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -2,8 +2,8 @@ from .models import BaseGroup, Group, GroupList, GroupListBase, User, UserList from .public import ( - EXTERNALLY_PUBLIC_GROUP, - INTERNALLY_PUBLIC_GROUP, + INTERNAL_GROUP, + PUBLIC_GROUP, refresh_public_groups, ) @@ -11,8 +11,8 @@ "Group", "GroupList", "GroupListBase", - "INTERNALLY_PUBLIC_GROUP", - "EXTERNALLY_PUBLIC_GROUP", + "INTERNAL_GROUP", + "PUBLIC_GROUP", "refresh_public_groups", "User", "BaseGroup", diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py index 03541f4..b63620e 100644 --- a/dsms/knowledge/groups/public.py +++ b/dsms/knowledge/groups/public.py @@ -17,7 +17,7 @@ # # NOTE: These constants are initialised at import time using whatever config is # available then (env-vars or defaults). If a DSMS instance is later created -# with a Configuration that overrides id_internally_public / id_externally_public, +# with a Configuration that overrides id_internal / id_public, # call refresh_public_groups(config) to keep the constants in sync. @@ -25,22 +25,20 @@ def _make_public_groups(cfg=None): if cfg is None: cfg = Session.dsms.config if Session.dsms else BaseConfiguration() return ( - Group(id=cfg.id_internally_public, name=cfg.label_internally_public), - Group(id=cfg.id_externally_public, name=cfg.label_externally_public), + Group(id=cfg.id_internal, name=cfg.label_internal), + Group(id=cfg.id_public, name=cfg.label_public), ) -INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups() +INTERNAL_GROUP, PUBLIC_GROUP = _make_public_groups() def refresh_public_groups(config=None) -> None: """Re-create the public group constants from the given (or current) config. Call this after constructing a DSMS instance whose Configuration overrides - id_internally_public or id_externally_public so that the module-level - constants stay in sync with the running configuration. + id_internal or id_public so that the module-level constants stay in sync + with the running configuration. """ - global INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP - INTERNALLY_PUBLIC_GROUP, EXTERNALLY_PUBLIC_GROUP = _make_public_groups( - config - ) + global INTERNAL_GROUP, PUBLIC_GROUP + INTERNAL_GROUP, PUBLIC_GROUP = _make_public_groups(config) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index 216c78d..ca09180 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -1,7 +1,7 @@ """KItem Access Property Module""" from enum import Enum, auto -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional from pydantic import BaseModel, Field, field_serializer, field_validator @@ -25,7 +25,6 @@ class Role(int, Enum): MEMBER = auto() CONTRIBUTOR = auto() OWNER = auto() - ADMIN = auto() class RoleMapping(List[OperationType], Enum): @@ -39,12 +38,6 @@ class RoleMapping(List[OperationType], Enum): ] MEMBER = [OperationType.READ] CONTRIBUTOR = [OperationType.READ, OperationType.UPDATE] - ADMIN = [ - OperationType.READ, - OperationType.UPDATE, - OperationType.DELETE, - OperationType.MANAGE, - ] @classmethod def get_operations(cls, role: Role) -> List[OperationType]: @@ -107,6 +100,16 @@ class BaseAccessProperty(BaseModel): example=RoleMapping.OWNER, ) + @field_validator("role", mode="before") + @classmethod + def parse_role(cls, v): + """Accept string names (e.g. 'owner') or legacy integer values.""" + if isinstance(v, str): + return Role[v.upper()] + if isinstance(v, int): + return Role(v) + return v + @property def access_level(self) -> List[OperationType]: """Set access level based on role""" @@ -116,10 +119,10 @@ def access_level(self) -> List[OperationType]: def serialize_role_json(self, value: Role, _info): """Serialize role to JSON""" if _info.mode == "python": - response = value.name # Python mode: use human-readable name - else: - response = value.value # JSON/wire mode: use integer value - return response + return value.name # Python mode: uppercase name + return ( + value.name.lower() + ) # wire mode: "member", "contributor", "owner" class UserAccessProperty(BaseAccessProperty): @@ -145,13 +148,20 @@ class GroupAccessProperty(BaseAccessProperty): class KItemAccessProperties(BaseModel): """KItem Access Properties Model""" + visibility: Literal["private", "internal", "public"] = Field( + "private", + description=( + "Visibility level: private (explicit access only), " + "internal (all authenticated users), public (everyone)." + ), + ) user_access: Optional[List[UserAccessProperty]] = Field( [], description="List of user access properties.", ) group_access: Optional[List[GroupAccessProperty]] = Field( [], - description="List of group access properties.", + description="List of group access properties (special visibility groups are excluded).", ) def __str__(self) -> str: diff --git a/tests/test_access.py b/tests/test_access.py index e21ec94..ddc8d05 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -30,7 +30,7 @@ def sample_user_access() -> List[UserAccessProperty]: def sample_group_access() -> List[GroupAccessProperty]: """Create sample group access properties""" return [ - GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group1", role=Role.OWNER), GroupAccessProperty(group_id="group2", role=Role.MEMBER), ] @@ -78,15 +78,15 @@ def test_minimum_access_level(): def test_maximum_access_level(): """Test max_access_level method""" - assert RoleMapping.max_access_level(OperationType.READ) == Role.ADMIN.value + assert RoleMapping.max_access_level(OperationType.READ) == Role.OWNER.value assert ( - RoleMapping.max_access_level(OperationType.UPDATE) == Role.ADMIN.value + RoleMapping.max_access_level(OperationType.UPDATE) == Role.OWNER.value ) assert ( - RoleMapping.max_access_level(OperationType.DELETE) == Role.ADMIN.value + RoleMapping.max_access_level(OperationType.DELETE) == Role.OWNER.value ) assert ( - RoleMapping.max_access_level(OperationType.MANAGE) == Role.ADMIN.value + RoleMapping.max_access_level(OperationType.MANAGE) == Role.OWNER.value ) @@ -106,19 +106,6 @@ def test_access_level_contributor(): assert prop.role.value == Role.CONTRIBUTOR.value -def test_access_level_admin(): - """Test access_level property for ADMIN role""" - prop = BaseAccessProperty(role=Role.ADMIN) - expected = [ - OperationType.READ, - OperationType.UPDATE, - OperationType.DELETE, - OperationType.MANAGE, - ] - assert prop.access_level == expected - assert prop.role.value == Role.ADMIN.value - - @pytest.mark.usefixtures("access_properties") def test_by_user_property(access_properties): """Test by_user property returns correct user mapping""" @@ -146,9 +133,9 @@ def test_by_group_property(access_properties): assert "group1" in result assert "group2" in result - assert result["group1"].role == Role.ADMIN + assert result["group1"].role == Role.OWNER assert result["group2"].role == Role.MEMBER - assert result["group1"].role.value == Role.ADMIN.value + assert result["group1"].role.value == Role.OWNER.value assert result["group2"].role.value == Role.MEMBER.value @@ -177,8 +164,8 @@ def test_operation_by_group_property(access_properties): """Test operation_by_group property returns correct operation mapping""" result = access_properties.operation_by_group - # group1 (ADMIN): READ, UPDATE, DELETE, MANAGE - # group2 (USER): READ + # group1 (OWNER): READ, UPDATE, DELETE, MANAGE + # group2 (MEMBER): READ expected_read = ["group1", "group2"] expected_update = ["group1"] @@ -230,16 +217,16 @@ def test_operation_by_group_multiple_same_operation(): group_access = [ GroupAccessProperty(group_id="group1", role=Role.MEMBER), GroupAccessProperty(group_id="group2", role=Role.MEMBER), - GroupAccessProperty(group_id="group3", role=Role.ADMIN), + GroupAccessProperty(group_id="group3", role=Role.OWNER), ] props = KItemAccessProperties(group_access=group_access) result = props.operation_by_group # All groups should have READ access assert set(result[OperationType.READ]) == {"group1", "group2", "group3"} - # Only group3 (ADMIN) should have MANAGE access + # Only group3 (OWNER) should have MANAGE access assert result[OperationType.MANAGE] == ["group3"] - assert props.group_by_role[Role.ADMIN] == ["group3"] + assert props.group_by_role[Role.OWNER] == ["group3"] assert props.group_by_role[Role.MEMBER] == ["group1", "group2"] @@ -289,7 +276,7 @@ def test_duplicate_user_ids_raises_error(): def test_duplicate_group_ids_raises_error(): """Test that duplicate group IDs raise ValueError""" group_access = [ - GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group1", role=Role.OWNER), GroupAccessProperty(group_id="group2", role=Role.MEMBER), GroupAccessProperty( group_id="group1", role=Role.CONTRIBUTOR @@ -314,7 +301,7 @@ def test_both_user_and_group_duplicates_raises_multiple_errors(): UserAccessProperty(user_id="user1", role=Role.MEMBER), # Duplicate ] group_access = [ - GroupAccessProperty(group_id="group1", role=Role.ADMIN), + GroupAccessProperty(group_id="group1", role=Role.OWNER), GroupAccessProperty(group_id="group1", role=Role.MEMBER), # Duplicate ] diff --git a/tests/test_access_extended.py b/tests/test_access_extended.py index 4ea34de..29c1d1d 100644 --- a/tests/test_access_extended.py +++ b/tests/test_access_extended.py @@ -17,14 +17,13 @@ def test_role_ordering(): - """Role integer values must be strictly ascending: USER < CONTRIBUTOR < OWNER < ADMIN.""" - assert Role.MEMBER < Role.CONTRIBUTOR < Role.OWNER < Role.ADMIN + """Role integer values must be strictly ascending: MEMBER < CONTRIBUTOR < OWNER.""" + assert Role.MEMBER < Role.CONTRIBUTOR < Role.OWNER def test_role_gte_comparison(): """>= on Role values must work correctly for threshold checks.""" assert Role.OWNER >= Role.CONTRIBUTOR - assert Role.ADMIN >= Role.OWNER assert not (Role.MEMBER >= Role.CONTRIBUTOR) @@ -44,7 +43,7 @@ def test_max_access_level_returns_role_instance(): """max_access_level must return a Role member, not a plain int.""" result = RoleMapping.max_access_level(OperationType.READ) assert isinstance(result, Role) - assert result is Role.ADMIN + assert result is Role.OWNER @pytest.mark.parametrize( @@ -69,9 +68,9 @@ def test_min_access_level_correct_role(operation, expected_min): OperationType.MANAGE, ], ) -def test_max_access_level_is_admin(operation): - """ADMIN always holds the maximum access level for every mapped operation.""" - assert RoleMapping.max_access_level(operation) is Role.ADMIN +def test_max_access_level_is_owner(operation): + """OWNER always holds the maximum access level for every mapped operation.""" + assert RoleMapping.max_access_level(operation) is Role.OWNER # --------------------------------------------------------------------------- @@ -111,7 +110,7 @@ def test_error_message_lists_valid_operations(): def test_serialize_role_json_mode(): - """Python mode → name string; JSON/wire mode → integer value.""" + """Python mode → uppercase name string; JSON/wire mode → lowercase name string.""" prop = UserAccessProperty(user_id="u1", role=Role.OWNER) python_dump = prop.model_dump(mode="python") @@ -119,8 +118,8 @@ def test_serialize_role_json_mode(): assert isinstance(python_dump["role"], str) json_dump = prop.model_dump(mode="json") - assert json_dump["role"] == Role.OWNER.value - assert isinstance(json_dump["role"], int) + assert json_dump["role"] == "owner" + assert isinstance(json_dump["role"], str) # --------------------------------------------------------------------------- @@ -128,18 +127,18 @@ def test_serialize_role_json_mode(): # --------------------------------------------------------------------------- -def test_model_dump_json_produces_integer_roles(): - """model_dump(mode='json') must produce integer role values for the wire format.""" +def test_model_dump_json_produces_string_roles(): + """model_dump(mode='json') must produce lowercase string role values for the wire format.""" props = KItemAccessProperties( user_access=[UserAccessProperty(user_id="u1", role=Role.OWNER)], group_access=[GroupAccessProperty(group_id="g1", role=Role.MEMBER)], ) payload = props.model_dump(mode="json") - assert payload["user_access"][0]["role"] == Role.OWNER.value - assert isinstance(payload["user_access"][0]["role"], int) - assert payload["group_access"][0]["role"] == Role.MEMBER.value - assert isinstance(payload["group_access"][0]["role"], int) + assert payload["user_access"][0]["role"] == "owner" + assert isinstance(payload["user_access"][0]["role"], str) + assert payload["group_access"][0]["role"] == "member" + assert isinstance(payload["group_access"][0]["role"], str) def test_model_dump_python_produces_string_roles(): @@ -153,14 +152,14 @@ def test_model_dump_python_produces_string_roles(): def test_round_trip_from_backend_dict(): - """A payload as returned by the backend (integer roles) must round-trip correctly.""" + """A payload as returned by the backend (string roles) must round-trip correctly.""" backend_payload = { "user_access": [ - {"user_id": "alice", "role": Role.OWNER.value}, - {"user_id": "bob", "role": Role.MEMBER.value}, + {"user_id": "alice", "role": "owner"}, + {"user_id": "bob", "role": "member"}, ], "group_access": [ - {"group_id": "dsms:internally-public", "role": Role.MEMBER.value}, + {"group_id": "dsms:internal", "role": "member"}, ], } @@ -168,17 +167,17 @@ def test_round_trip_from_backend_dict(): assert props.by_user["alice"].role is Role.OWNER assert props.by_user["bob"].role is Role.MEMBER - assert props.by_group["dsms:internally-public"].role is Role.MEMBER + assert props.by_group["dsms:internal"].role is Role.MEMBER # Serialise back and verify identity re_serialised = props.model_dump(mode="json") assert re_serialised["user_access"][0] == { "user_id": "alice", - "role": Role.OWNER.value, + "role": "owner", } assert re_serialised["group_access"][0] == { - "group_id": "dsms:internally-public", - "role": Role.MEMBER.value, + "group_id": "dsms:internal", + "role": "member", } diff --git a/tests/test_groups.py b/tests/test_groups.py index 52dbcd4..737cc70 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -13,8 +13,8 @@ UserList, ) from dsms.knowledge.groups.public import ( - EXTERNALLY_PUBLIC_GROUP, - INTERNALLY_PUBLIC_GROUP, + INTERNAL_GROUP, + PUBLIC_GROUP, ) # --------------------------------------------------------------------------- @@ -161,21 +161,21 @@ def test_userlist_getitem_missing_raises(): # --------------------------------------------------------------------------- -def test_internally_public_group_id_uses_hyphen(): - """ID must use hyphens, matching the BaseConfiguration default.""" - assert INTERNALLY_PUBLIC_GROUP.id == "dsms:internally-public" +def test_internal_group_id(): + """INTERNAL_GROUP.id must match the BaseConfiguration default.""" + assert INTERNAL_GROUP.id == "dsms:internal" -def test_externally_public_group_id_uses_hyphen(): - assert EXTERNALLY_PUBLIC_GROUP.id == "dsms:externally-public" +def test_public_group_id(): + assert PUBLIC_GROUP.id == "dsms:public" -def test_internally_public_group_has_name(): - assert INTERNALLY_PUBLIC_GROUP.name != "" +def test_internal_group_has_name(): + assert INTERNAL_GROUP.name != "" -def test_externally_public_group_has_name(): - assert EXTERNALLY_PUBLIC_GROUP.name != "" +def test_public_group_has_name(): + assert PUBLIC_GROUP.name != "" def test_refresh_public_groups_uses_custom_config(): @@ -183,23 +183,23 @@ def test_refresh_public_groups_uses_custom_config(): from dsms.core.configuration import BaseConfiguration from dsms.knowledge.groups import public as pub - original_id = pub.INTERNALLY_PUBLIC_GROUP.id + original_id = pub.INTERNAL_GROUP.id custom_cfg = BaseConfiguration( - id_internally_public="custom:internal", - id_externally_public="custom:external", - label_internally_public="Custom Internal", - label_externally_public="Custom External", + id_internal="custom:internal", + id_public="custom:external", + label_internal="Custom Internal", + label_public="Custom External", ) pub.refresh_public_groups(custom_cfg) - assert pub.INTERNALLY_PUBLIC_GROUP.id == "custom:internal" - assert pub.EXTERNALLY_PUBLIC_GROUP.id == "custom:external" - assert pub.INTERNALLY_PUBLIC_GROUP.name == "Custom Internal" + assert pub.INTERNAL_GROUP.id == "custom:internal" + assert pub.PUBLIC_GROUP.id == "custom:external" + assert pub.INTERNAL_GROUP.name == "Custom Internal" # Restore defaults so other tests are not affected pub.refresh_public_groups() - assert pub.INTERNALLY_PUBLIC_GROUP.id == original_id + assert pub.INTERNAL_GROUP.id == original_id def test_refresh_public_groups_without_arg_restores_defaults( @@ -209,8 +209,8 @@ def test_refresh_public_groups_without_arg_restores_defaults( from dsms.knowledge.groups import public as pub pub.refresh_public_groups() - assert pub.INTERNALLY_PUBLIC_GROUP.id == "dsms:internally-public" - assert pub.EXTERNALLY_PUBLIC_GROUP.id == "dsms:externally-public" + assert pub.INTERNAL_GROUP.id == "dsms:internal" + assert pub.PUBLIC_GROUP.id == "dsms:public" # --------------------------------------------------------------------------- From 21f714967e848b7d9ab97675d7baf1e3a84f40fe Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Sun, 7 Jun 2026 16:54:17 -0400 Subject: [PATCH 37/48] Move pytest-nbmake to docs extras and refresh tutorial notebooks --- docs/dsms_sdk/tutorials/1_introduction.ipynb | 4006 +++++++++++++++-- docs/dsms_sdk/tutorials/2_creation.ipynb | 502 ++- docs/dsms_sdk/tutorials/3_updating.ipynb | 329 +- docs/dsms_sdk/tutorials/4_deletion.ipynb | 693 ++- docs/dsms_sdk/tutorials/5_search.ipynb | 1419 +++++- docs/dsms_sdk/tutorials/6_apps.ipynb | 730 ++- docs/dsms_sdk/tutorials/7_ktypes.ipynb | 920 +++- .../dsms_sdk/tutorials/8_kitem_contexts.ipynb | 271 +- setup.cfg | 2 +- 9 files changed, 7861 insertions(+), 1011 deletions(-) diff --git a/docs/dsms_sdk/tutorials/1_introduction.ipynb b/docs/dsms_sdk/tutorials/1_introduction.ipynb index bb26faa..4dc8c60 100644 --- a/docs/dsms_sdk/tutorials/1_introduction.ipynb +++ b/docs/dsms_sdk/tutorials/1_introduction.ipynb @@ -22,7 +22,14 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:51:54.747731Z", + "iopub.status.busy": "2026-06-07T20:51:54.747573Z", + "iopub.status.idle": "2026-06-07T20:51:55.425708Z", + "shell.execute_reply": "2026-06-07T20:51:55.424734Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS" @@ -38,7 +45,14 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:51:55.429267Z", + "iopub.status.busy": "2026-06-07T20:51:55.428914Z", + "iopub.status.idle": "2026-06-07T20:51:55.957899Z", + "shell.execute_reply": "2026-06-07T20:51:55.956590Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -62,220 +76,186 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:51:55.961270Z", + "iopub.status.busy": "2026-06-07T20:51:55.961068Z", + "iopub.status.idle": "2026-06-07T20:51:57.124354Z", + "shell.execute_reply": "2026-06-07T20:51:57.123030Z" + } + }, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `CKAN Resource`. Cannot check if value is of correct type.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `CKAN Download URL`. Cannot check if value is of correct type.\n", - " warnings.warn(\n", - "/app/dsms/knowledge/kitem.py:692: UserWarning: No webform was defined for entry `Media Type`. Cannot check if value is of correct type.\n", - " warnings.warn(\n" - ] - }, { "data": { "text/plain": [ "kitems:\n", - "- id: 94f54c99-ddb0-4462-8fe3-06a16aa105ec\n", - " name: Tensile_C_09.lis\n", - " ktype_id: web-ressource\n", - " slug: tensile_c_09lis-94f54c99\n", - " annotations:\n", - " - iri: https://www.iana.org/assignments/media-types/text/csv\n", - " label: csv\n", - " namespace: https://www.iana.org/assignments/media-types/text\n", - " attachments: []\n", - " linked_kitems:\n", - " - is_incoming: false\n", - " label: Has Part\n", - " kitem:\n", - " id: 02ce95a9-50b5-4d79-a68d-49214524aa41\n", - " name: KupferDigital_BAM_Tensile_C_09\n", - " ktype_id: dataset\n", - " slug: kupferdigital_bam_tensile_c_09-02ce95a9\n", - " iri: http://purl.org/dc/terms/hasPart\n", + "- id: 032a3d86-4705-47c0-969e-3251db82a1a6\n", + " name: my tensile test experiment\n", + " ktype_id: dataset\n", + " slug: mytensiletestexperiment-032a3d86\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: dummy_data.csv\n", + " linked_kitems: []\n", " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: true\n", " contacts: []\n", - " created_at: 2025-07-02 15:49:23.206464\n", - " updated_at: 2025-07-02 15:49:23.206464\n", - " external_links:\n", - " - label: CKAN download URL\n", - " url: https://ckan.kupferdigital.org/dataset/02ce95a9-50b5-4d79-a68d-49214524aa41/resource/94f54c99-ddb0-4462-8fe3-06a16aa105ec/download/tensile_c_09.lis\n", - " - label: CKAN source\n", - " url: https://ckan.kupferdigital.org/dataset/02ce95a9-50b5-4d79-a68d-49214524aa41/resource/94f54c99-ddb0-4462-8fe3-06a16aa105ec\n", - " apps: []\n", - " summary: https://ckan.kupferdigital.org/dataset/02ce95a9-50b5-4d79-a68d-49214524aa41/resource/94f54c99-ddb0-4462-8fe3-06a16aa105ec\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id17514713629953u7k9z\n", - " name: Misc\n", - " entries:\n", - " - id: id1751471362995mom5gj\n", - " type: Text\n", - " label: CKAN Resource\n", - " value: https://ckan.kupferdigital.org/dataset/02ce95a9-50b5-4d79-a68d-49214524aa41/resource/94f54c99-ddb0-4462-8fe3-06a16aa105ec\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id1751471362995wkglsz\n", - " type: Text\n", - " label: CKAN Download URL\n", - " value: https://ckan.kupferdigital.org/dataset/02ce95a9-50b5-4d79-a68d-49214524aa41/resource/94f54c99-ddb0-4462-8fe3-06a16aa105ec/download/tensile_c_09.lis\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", + " created_at: 2026-06-05 15:55:53.758604\n", + " updated_at: 2026-06-05 15:55:53.758604\n", + " external_links: []\n", + " apps:\n", + " - executable: testapp2\n", + " title: data2rdf\n", + " description: null\n", + " tags: null\n", + " additional_properties:\n", + " triggerUponUpload: true\n", + " triggerUponUploadFileExtensions:\n", + " - .csv\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", " contexts: []\n", - "- id: 049821c7-2fb4-41b6-89f9-eba2c76d580a\n", - " name: KupferDigital_7F21109_ID3243_Pos._b_EDX\n", - " ktype_id: dataset\n", - " slug: kupferdigital_7f21109_id3243_pos_b_edx-049821c7\n", + "- id: 05884075-236c-483a-8427-1c9cd8886a04\n", + " name: test 2\n", + " ktype_id: metal-sheet\n", + " slug: test-2\n", + " avatar_exists: false\n", + " has_contexts: false\n", " annotations: []\n", - " attachments: []\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", " linked_kitems:\n", " - is_incoming: false\n", " label: Has Part\n", " kitem:\n", - " id: 3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", - " name: KupferDigi\n", - " ktype_id: web-ressource\n", - " slug: kupferdigi-3d0822c9\n", + " id: 5f2868e2-1196-4fb9-adb7-3b3d68e0a1f3\n", + " name: Specimen Extraction 2618A\n", + " ktype_id: manufacturing-process\n", + " slug: specimen-extraction-2618a\n", + " avatar_exists: true\n", + " has_contexts: false\n", " iri: http://purl.org/dc/terms/hasPart\n", " - is_incoming: false\n", " label: Has Part\n", " kitem:\n", - " id: f633b4b3-3ca6-447d-9e9b-ed7154611aba\n", - " name: Kupferdigital CKAN Instance\n", - " ktype_id: external-data-source\n", - " slug: kupferdigitalckaninstance-f633b4b3\n", - " iri: http://purl.org/dc/terms/hasPart\n", - " - is_incoming: true\n", - " label: Has Part\n", - " kitem:\n", - " id: 97e4e09b-1c84-428d-a007-f17c9814ad15\n", - " name: KupferDigi\n", - " ktype_id: web-ressource\n", - " slug: kupferdigi-97e4e09b\n", + " id: bb98191c-9695-4980-a655-cad03abde223\n", + " name: Metal sheet test\n", + " ktype_id: metal-sheet\n", + " slug: metal-sheet-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", " iri: http://purl.org/dc/terms/hasPart\n", " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: true\n", " contacts: []\n", - " created_at: 2025-07-02 15:49:37.710531\n", - " updated_at: 2025-07-02 15:49:37.710531\n", - " external_links:\n", - " - label: CKAN source\n", - " url: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a\n", + " created_at: 2026-06-04 18:35:46.248292\n", + " updated_at: 2026-06-04 18:35:46.248292\n", + " external_links: []\n", " apps: []\n", - " summary: eds measurements on a diffusion specimen\n", - " user_groups: []\n", - " custom_properties:\n", - " content:\n", - " sections:\n", - " - id: id1751471401085iwiex6\n", - " name: Misc\n", - " entries:\n", - " - id: id1751471401085lwpseh\n", - " type: Knowledge item\n", - " label: Resources\n", - " value:\n", - " - id: 3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", - " name: KupferDigi\n", - " ktype_id: web-ressource\n", - " slug: kupferdigi-3d0822c9\n", - " - id: 97e4e09b-1c84-428d-a007-f17c9814ad15\n", - " name: KupferDigi\n", - " ktype_id: web-ressource\n", - " slug: kupferdigi-97e4e09b\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " - id: id17514714010858safjx\n", - " type: Knowledge item\n", - " label: Publishing Organisation\n", - " value:\n", - " - id: c6a0740a-3bb2-4661-a5d7-67488fa061a7\n", - " name: fem-organization\n", - " ktype_id: organization\n", - " slug: fem-organization-c6a0740a\n", - " measurementUnit: null\n", - " relationMapping: null\n", - " required: false\n", - " rdf_exists: true\n", - " contexts: []\n", - "- id: 3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", - " name: KupferDigi\n", - " ktype_id: web-ressource\n", - " slug: kupferdigi-3d0822c9\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " - role: CONTRIBUTOR\n", + " user_id: b76fa8d6-20d8-4a7e-a959-a975082b1c63\n", + " group_access: []\n", + " contexts:\n", + " - id: 249e1cdf-f584-4568-86cb-198757c66c15\n", + " name: metal sheet batch test\n", + " ktype_id: metal-sheet-batch\n", + " slug: metal-sheet-batch-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + "- id: 1053cd9b-221a-40cc-adb6-517cdcaa174c\n", + " name: My creep speciment\n", + " ktype_id: creep-specimen\n", + " slug: my-creep-speciment\n", + " avatar_exists: false\n", + " has_contexts: false\n", " annotations:\n", - " - iri: https://www.iana.org/assignments/media-types/application/vnd.ms-excel\n", - " label: vnd.ms-excel\n", - " namespace: https://www.iana.org/assignments/media-types/application\n", - " attachments: []\n", - " linked_kitems:\n", - " - is_incoming: true\n", - " label: Has Part\n", - " kitem:\n", - " id: 049821c7-2fb4-41b6-89f9-eba2c76d580a\n", - " name: KupferDigital_7F21109_ID3243_Pos._b_EDX\n", - " ktype_id: dataset\n", - " slug: kupferdigital_7f21109_id3243_pos_b_edx-049821c7\n", - " iri: http://purl.org/dc/terms/hasPart\n", + " - iri: https://w3id.org/steel/ProcessOntology/PercentageExtension\n", + " label: PercentageExtension\n", + " namespace: steelontology\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems: []\n", " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: true\n", " contacts: []\n", - " created_at: 2025-07-02 15:49:49.501334\n", - " updated_at: 2025-07-02 15:49:49.501334\n", - " external_links:\n", - " - label: CKAN source\n", - " url: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a/resource/3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", - " - label: CKAN download URL\n", - " url: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a/resource/3d0822c9-c87c-42c8-b390-8f392ffcea3f/download/kupferdigital_7f21109_id3243_pos._b_edx_rawdata.xlsx\n", + " created_at: 2026-05-28 08:49:52.034919\n", + " updated_at: 2026-05-28 08:49:52.034919\n", + " external_links: []\n", " apps: []\n", - " summary: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a/resource/3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", - " user_groups: []\n", + " summary: ''\n", " custom_properties:\n", " content:\n", " sections:\n", - " - id: id1751471389254oxwoza\n", - " name: Misc\n", + " - id: section-creep-gauge\n", + " name: Gauge Section\n", " entries:\n", - " - id: id1751471389254g2ojkx\n", - " type: Text\n", - " label: CKAN Resource\n", - " value: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a/resource/3d0822c9-c87c-42c8-b390-8f392ffcea3f\n", + " - id: input-creep-gauge-length\n", + " type: Number\n", + " label: Gauge length\n", + " value: null\n", " measurementUnit: null\n", - " relationMapping: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasGaugeLengthLo_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", " required: false\n", - " - id: id1751471389254kf8g0k\n", - " type: Text\n", - " label: CKAN Download URL\n", - " value: https://ckan.kupferdigital.org/dataset/049821c7-2fb4-41b6-89f9-eba2c76d580a/resource/3d0822c9-c87c-42c8-b390-8f392ffcea3f/download/kupferdigital_7f21109_id3243_pos._b_edx_rawdata.xlsx\n", + " - id: input-creep-gauge-diameter\n", + " type: Number\n", + " label: Gauge diameter\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDiameter_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: section-creep-grip\n", + " name: Grip Ends\n", + " entries:\n", + " - id: input-creep-grip-type\n", + " type: Select\n", + " label: Grip type\n", + " value: null\n", " measurementUnit: null\n", - " relationMapping: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleType_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", " required: false\n", - " - id: id1751471389254gdr583\n", + " - id: input-creep-thread-spec\n", " type: Text\n", - " label: Media Type\n", - " value: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\n", + " label: Thread specification\n", + " value: null\n", " measurementUnit: null\n", - " relationMapping: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", " required: false\n", - " rdf_exists: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", " contexts: []\n", - "total_count: 356" + "total_count: 38" ] }, "execution_count": 3, @@ -301,244 +281,3596 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:51:57.152498Z", + "iopub.status.busy": "2026-06-07T20:51:57.152317Z", + "iopub.status.idle": "2026-06-07T20:51:57.279821Z", + "shell.execute_reply": "2026-06-07T20:51:57.278583Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "ktype:\n", - " id: expert\n", - " created_at: '2025-06-26T13:27:43.597461'\n", - " updated_at: '2025-06-26T13:27:43.597461'\n", - "\n", - "ktype:\n", - " id: dataset\n", - " name: dataset\n", - " created_at: '2025-06-26T13:27:43.597461'\n", - " updated_at: '2025-06-26T13:27:43.597461'\n", - "\n", "ktype:\n", " id: dataset-catalog\n", " name: dataset catalog\n", - " created_at: '2025-06-26T13:27:43.597461'\n", - " updated_at: '2025-06-26T13:27:43.597461'\n", + " created_at: '2024-07-28T18:48:21.024154'\n", + " updated_at: '2024-07-28T18:48:21.024154'\n", "\n", "ktype:\n", - " id: external-data-source\n", - " name: External Data Source\n", - " created_at: '2025-07-02T09:43:27.121927'\n", - " updated_at: '2025-07-02T09:43:27.121927'\n", + " id: dataset\n", + " name: Dataset\n", + " process_schema_id: d5def9c4-edd2-491e-ae32-b99e3c0798c1\n", + " process_schema:\n", + " id: d5def9c4-edd2-491e-ae32-b99e3c0798c1\n", + " name: daaad\n", + " spec:\n", + " - id: engineered-raw-material\n", + " label: Engineered raw material\n", + " is_child: false\n", + " mappings: []\n", + " children: []\n", + " created_at: 2025-08-15 14:24:31.597368\n", + " updated_at: 2025-08-15 14:24:31.597368\n", + " created_at: '2024-07-28T18:48:21.024154'\n", + " updated_at: '2026-05-13T10:16:27.749520'\n", "\n", "ktype:\n", - " id: app\n", - " name: app\n", - " webform_schema_id: a366408f-7949-42ac-8e9b-686f788cbd86\n", + " id: chemical-element\n", + " name: Chemical element\n", + " webform_schema_id: dccbd36b-33c7-4300-b29a-c8c42be0103d\n", " webform_schema:\n", - " id: a366408f-7949-42ac-8e9b-686f788cbd86\n", - " name: application\n", + " id: dccbd36b-33c7-4300-b29a-c8c42be0103d\n", + " name: Chemical element\n", " spec:\n", " semantics_enabled: true\n", " sections_enabled: false\n", " class_mapping:\n", - " - https://w3id.org/emmo#EMMO_3b031fa9_8623_4ea5_8b57_bcafb70c5c8b\n", + " - https://w3id.org/emmo#EMMO_4f40def1_3cd7_4067_9596_541e9a5134cf\n", " sections:\n", - " - id: id11ee168baf44a8\n", - " name: ''\n", + " - id: id1081805716ece8\n", + " name: Untitled Section\n", " inputs:\n", - " - id: idf0096545188b98\n", - " label: Application name\n", + " - id: id2432a7770fd35\n", + " label: Symbol\n", " widget: Text\n", " required: false\n", + " hint: Symbol of the chemical element\n", " hidden: false\n", " ignore: false\n", " select_options: []\n", " relation_mapping:\n", - " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier\n", - " label: has Identifier\n", - " type: data_property\n", + " iri: https://w3id.org/emmo#EMMO_79c0edfa_06f9_5149_b754_28c589035b8a\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_d357e0dd_3497_4590_af6f_7954db7fecf7\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idd1d4b6bc1d88f8\n", + " label: Mass density\n", + " widget: Number\n", + " required: false\n", + " hint: \"Mass density of the pure element at 20 \\xB0C\"\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: g/cm^3\n", + " namespace: None\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " range_options:\n", + " min: 0\n", + " max: 100\n", + " step: 1\n", + " range: false\n", + " - id: id8947278e4717e\n", + " label: Atomic weigth\n", + " widget: Number\n", + " required: false\n", + " hint: Weigth of a single atom of the element\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: u\n", + " namespace: None\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " range_options:\n", + " min: 0\n", + " max: 100\n", + " step: 1\n", + " range: false\n", + " - id: id5afa099e502808\n", + " label: Electronegativity\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " range_options:\n", + " min: 0\n", + " max: 100\n", + " step: 1\n", + " range: false\n", + " - id: id9ca4bd4c66c7d8\n", + " label: Atomic number\n", + " widget: Number\n", + " required: false\n", + " hint: Nuclear charge number (symbol Z) of a chemical element is the charge\n", + " number of an atomic nucleus.\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " range_options:\n", + " min: 0\n", + " max: 100\n", + " step: 1\n", + " range: false\n", + " - id: id8ff58e11b41d1\n", + " label: Category\n", + " widget: Select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Alkali metals\n", + " disabled: false\n", + " - key: option2\n", + " label: Alkaline earth metals\n", + " disabled: false\n", + " - key: option3\n", + " label: Transition elements\n", + " disabled: false\n", + " - key: option4\n", + " label: Pnictogens\n", + " disabled: false\n", + " - key: option5\n", + " label: Chalcogens\n", + " disabled: false\n", + " - key: option6\n", + " label: Halogens\n", + " disabled: false\n", + " - key: option7\n", + " label: Noble gases\n", + " disabled: false\n", + " - key: option8\n", + " label: Lanthanoids\n", + " disabled: false\n", + " - key: option9\n", + " label: Actinoids\n", + " disabled: false\n", + " - key: option10\n", + " label: Rare-earth metals\n", + " disabled: false\n", + " - key: option11\n", + " label: Inner transition elements\n", + " disabled: false\n", + " - key: option12\n", + " label: Nonmetals\n", + " disabled: false\n", + " - key: option13\n", + " label: Main-group elements\n", + " disabled: false\n", + " - key: option14\n", + " label: Metals\n", + " disabled: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " range_options:\n", + " min: 0\n", + " max: 100\n", + " step: 1\n", + " range: false\n", + " - id: id6723afc60ded7\n", + " label: CAS registry number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/ORCHESTER/CAS-Number\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: idcb35894915be5\n", + " label: Critical raw material according to EU definition 2023\n", + " widget: Checkbox\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/ORCHESTER/CriticalRawMaterial\n", + " inverse: false\n", " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", " hidden: false\n", - " created_at: '2025-07-15T14:28:48.236596'\n", - " updated_at: '2025-07-15T14:28:48.236596'\n", - " created_at: '2025-06-26T13:27:43.597461'\n", - " updated_at: '2025-07-15T14:29:07.123972'\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-10-23T12:27:02.976940'\n", + " updated_at: '2025-03-30T08:39:19.556285'\n", "\n", "ktype:\n", - " id: web-ressource\n", - " name: Web Ressource\n", - " webform_schema_id: 6f897085-20ae-4f58-88cb-4ecfaa59e251\n", + " id: engineered-raw-material\n", + " name: Engineered raw material\n", + " webform_schema_id: 2a6bfb9f-0e94-4a34-933f-91d99f5475dc\n", " webform_schema:\n", - " id: 6f897085-20ae-4f58-88cb-4ecfaa59e251\n", - " name: Web Ressource\n", + " id: 2a6bfb9f-0e94-4a34-933f-91d99f5475dc\n", + " name: Engineered raw material\n", " spec:\n", " semantics_enabled: true\n", - " sections_enabled: false\n", + " sections_enabled: true\n", " class_mapping:\n", - " - http://www.w3.org/ns/dcat#Resource\n", + " - https://w3id.org/pmd/co/EngineeredMaterial\n", " sections:\n", - " - id: id34da047a592c98\n", + " - id: idc4618e5992561\n", " name: Untitled Section\n", " inputs:\n", - " - id: id053f055e718bf8\n", - " label: Access URL\n", + " - id: id8668ff07ea324\n", + " label: Name of the raw material\n", " widget: Text\n", " required: false\n", " hidden: false\n", " ignore: false\n", " select_options: []\n", " relation_mapping:\n", - " iri: http://www.w3.org/ns/dcat#accessURL\n", - " label: access address\n", - " type: property\n", + " iri: https://orchester.materials-data.space/ontology/eclass/HasBezeichnung\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: idc6aa4925f283b\n", + " name: 'Material classification '\n", + " inputs:\n", + " - id: id0eb116104e865\n", + " label: CAS number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasMaterialIdentifier_Object\n", + " type: object_property\n", + " class_iri: https://w3id.org/ORCHESTER/CAS-Number\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id2a6a94b7a6b86\n", + " label: Material class ID according to IEC 62474\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasMaterialIdentifier_Object\n", + " type: object_property\n", + " class_iri: https://w3id.org/ORCHESTER/IEC-62474-MaterialClassificationNumber\n", + " inverse: false\n", " multiple_selection: false\n", " hidden: false\n", - " created_at: '2025-07-15T13:58:53.020266'\n", - " updated_at: '2025-07-16T11:52:10.690939'\n", - " created_at: '2025-07-02T09:42:46.289536'\n", - " updated_at: '2025-07-16T11:51:03.826524'\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-27T06:58:34.114685'\n", + " updated_at: '2025-03-22T08:22:07.956056'\n", "\n", "ktype:\n", - " id: organization\n", - " name: organization\n", - " webform_schema_id: 0575cff0-e6d3-45ca-8474-ac5b402e522d\n", + " id: app\n", + " name: App\n", + " webform_schema_id: 7ef1f83c-82c5-4adb-bafb-da7f28bcbaa2\n", " webform_schema:\n", - " id: 0575cff0-e6d3-45ca-8474-ac5b402e522d\n", - " name: organization\n", + " id: 7ef1f83c-82c5-4adb-bafb-da7f28bcbaa2\n", + " name: app\n", " spec:\n", " semantics_enabled: true\n", " sections_enabled: false\n", - " class_mapping: []\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/ApplicationProgram\n", " sections:\n", - " - id: id318659d85714d8\n", - " name: ''\n", + " - id: ida00a19653fc4\n", + " name: Untitled Section\n", " inputs:\n", - " - id: idba9fb3fd5543a8\n", - " label: address\n", - " widget: Text\n", + " - id: id3be2dc8a3eb2b\n", + " label: Type of application\n", + " widget: Multi-select\n", " required: false\n", " hidden: false\n", " ignore: false\n", - " select_options: []\n", + " select_options:\n", + " - key: option1\n", + " label: Web application\n", + " disabled: false\n", + " - key: option2\n", + " label: Software application\n", + " disabled: false\n", + " - key: option3\n", + " label: Data processing application\n", + " disabled: false\n", " relation_mapping:\n", - " iri: https://w3id.org/steel/ProcessOntology/hasLocation\n", - " type: property\n", + " iri: https://w3id.org/ORCHESTER/HasApplicationType\n", + " type: data_property\n", + " inverse: false\n", " multiple_selection: false\n", " hidden: false\n", - " created_at: '2025-07-16T14:18:05.866631'\n", - " updated_at: '2025-07-16T14:18:05.866631'\n", - " created_at: '2025-07-16T14:18:05.992083'\n", - " updated_at: '2025-07-16T14:18:06.051750'\n", - "\n", - "ktype:\n", - " id: characterization-process\n", - " name: Characterization Process\n", - " process_schema_id: 4975067f-4804-4218-bf83-e48ff3990281\n", - " process_schema:\n", - " id: 4975067f-4804-4218-bf83-e48ff3990281\n", - " name: Characterization Process\n", - " spec:\n", - " - id: organization\n", - " label: organization\n", - " is_child: false\n", - " mappings:\n", - " - dst_ktype_id: app\n", - " relation_iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " relation_name: hasPart\n", - " children: []\n", - " - id: app\n", - " label: app\n", - " is_child: false\n", - " mappings:\n", - " - dst_ktype_id: web-ressource\n", - " relation_iri: https://w3id.org/emmo#EMMO_c4bace1d_4db0_4cd3_87e9_18122bae2840\n", - " relation_name: hasOutput\n", - " children:\n", - " - id: web-ressource\n", - " label: Web Ressource\n", - " is_child: true\n", - " mappings: []\n", - " children: []\n", - " created_at: 2025-07-16 14:18:06.312271\n", - " updated_at: 2025-07-16 14:18:06.312271\n", - " created_at: '2025-07-16T14:18:06.433199'\n", - " updated_at: '2025-07-16T14:18:06.495714'\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-07-28T18:48:21.024154'\n", + " updated_at: '2026-05-03T13:55:53.473588'\n", "\n", "ktype:\n", - " id: specimen\n", - " name: Specimen\n", - " webform_schema_id: 21164fb6-cc45-4e08-8f8b-467a749df54b\n", + " id: production-process\n", + " name: Production process\n", + " webform_schema_id: eafb9978-2fd7-4d1a-86f7-0d3c9b10f784\n", " webform_schema:\n", - " id: 21164fb6-cc45-4e08-8f8b-467a749df54b\n", - " name: Specimen\n", + " id: eafb9978-2fd7-4d1a-86f7-0d3c9b10f784\n", + " name: Production process\n", " spec:\n", " semantics_enabled: true\n", - " sections_enabled: false\n", + " sections_enabled: true\n", " class_mapping:\n", - " - https://w3id.org/pmd/co/Specimen\n", + " - https://w3id.org/ORCHESTER/ProductionProcess\n", " sections:\n", - " - id: idb424123144cdd8\n", - " name: Untitled Section\n", + " - id: id474a0e2154dfa\n", + " name: Basic information\n", " inputs:\n", - " - id: id6c76bbffe7ca78\n", - " label: Width\n", - " widget: Number\n", + " - id: id54f8f37ec00d5\n", + " label: Name of the production process\n", + " widget: Text\n", " required: false\n", " hidden: false\n", " ignore: false\n", " select_options: []\n", - " measurement_unit:\n", - " label: Millimetre\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " namespace: http://qudt.org/vocab/unit\n", " relation_mapping:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0\n", + " iri: https://orchester.materials-data.space/ontology/eclass/HasBezeichnung\n", + " type: data_property\n", + " inverse: false\n", " multiple_selection: false\n", " knowledge_type:\n", " - null\n", - " range_options:\n", - " min: 0\n", - " max: 1\n", - " step: 0.1\n", - " range: false\n", - " - id: id717d07130a7618\n", - " label: Length\n", - " widget: Slider\n", + " - id: id0c4d8631681ea\n", + " label: Production device\n", + " widget: Knowledge item\n", " required: false\n", " hidden: false\n", " ignore: false\n", " select_options: []\n", - " measurement_unit:\n", - " label: Millimetre\n", - " iri: http://qudt.org/vocab/unit/MilliM\n", - " namespace: http://qudt.org/vocab/unit\n", " relation_mapping:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_cd2cd0de_e0cc_4ef1_b27e_2e88db027bac\n", - " relation_mapping_extra:\n", - " iri: https://w3id.org/emmo#EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f\n", - " type: object_property\n", - " class_iri: https://w3id.org/emmo#EMMO_e4de48b1_dabb_4490_ac2b_040f926c64f0\n", + " iri: https://w3id.org/ORCHESTER/HasDevice\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type:\n", + " - production-device\n", + " - id: idac65a34241e1b\n", + " label: Operator\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasOperator\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type:\n", + " - expert\n", + " hidden: false\n", + " - id: id6165143256806\n", + " name: Input\n", + " inputs:\n", + " - id: id98a91c57b9725\n", + " label: Feedstock\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasInput\n", + " type: data_property\n", + " inverse: false\n", " multiple_selection: false\n", - " range_options:\n", - " min: 0\n", - " max: 1\n", - " step: 0.1\n", - " range: true\n", + " knowledge_type:\n", + " - engineered-raw-material\n", + " - srap\n", + " - engineered-material\n", " hidden: false\n", - " created_at: '2025-07-21T09:24:48.597715'\n", - " updated_at: '2025-07-21T13:56:38.225444'\n", - " created_at: '2025-07-21T09:20:12.746430'\n", - " updated_at: '2025-07-21T09:24:58.495720'\n", - "\n", - "ktype:\n", - " id: testingmachine\n", - " name: TestingMachine\n", - " created_at: '2025-07-22T12:41:50.505566'\n", - " updated_at: '2025-07-22T12:41:50.505566'\n", + " - id: ide70ac894011e5\n", + " name: Output\n", + " inputs:\n", + " - id: id046b57d591547\n", + " label: Production result (Output)\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasOutput\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - engineered-raw-material\n", + " hidden: false\n", + " - id: id0b092428a801f\n", + " name: Process parameters\n", + " inputs:\n", + " - id: iddde1dea6f3929\n", + " label: Target process parameters\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/parameters\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - dataset\n", + " - id: id364aaa5f47529\n", + " label: Target process parameters (freetext)\n", + " widget: Textarea\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/parameters\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id91b64ed5b453e\n", + " label: Process data\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/parameters\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type:\n", + " - dataset\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-27T00:37:48.409608'\n", + " updated_at: '2025-03-29T16:37:41.001343'\n", + "\n", + "ktype:\n", + " id: specimen\n", + " name: Specimen\n", + " webform_schema_id: 7199e339-2512-4aed-8b99-af495c0349d3\n", + " webform_schema:\n", + " id: 7199e339-2512-4aed-8b99-af495c0349d3\n", + " name: Specimen\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: http://purl.obolibrary.org/obo/OBI_0100051\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " inputs:\n", + " - id: input-specimen-type\n", + " label: Specimen type\n", + " widget: Text\n", + " required: false\n", + " hint: e.g. flat, round, notched\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleType_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-geometry\n", + " label: Specimen geometry\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceGeometry_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-width\n", + " label: Width\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasWidth_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-length\n", + " label: Length\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceLength_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-thickness\n", + " label: Thickness\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceThickness_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-diameter\n", + " label: Diameter\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDiameter_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-description\n", + " label: Additional description\n", + " widget: Textarea\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2026-06-04T16:44:35.090388'\n", + " created_at: '2024-10-23T13:59:30.448842'\n", + " updated_at: '2024-11-15T11:28:54.289602'\n", + "\n", + "ktype:\n", + " id: material-model\n", + " name: Material model\n", + " webform_schema_id: 615d6973-8911-4d48-92fa-a52f40cfbc4b\n", + " webform_schema:\n", + " id: 615d6973-8911-4d48-92fa-a52f40cfbc4b\n", + " name: Material model\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/material-model/MaterialModel\n", + " sections:\n", + " - id: id8fb799b97436a\n", + " name: Basic information\n", + " inputs:\n", + " - id: ida80c8421ee611\n", + " label: Name of the material model\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idaa801d028b0fe\n", + " label: Source document\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#titleWithOtherTitleInformation\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: document\n", + " - id: id0f16b1c4a9c\n", + " label: Short description\n", + " widget: Textarea\n", + " required: false\n", + " hint: Please add a short description of the material model\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://purl.org/dc/terms/description\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idb008410d0bcba\n", + " label: Geometrical linear or non-linear\n", + " widget: Select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Small strain theory\n", + " disabled: false\n", + " - key: option2\n", + " label: Large strain theory\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasModelProperty\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ida2ee67091b30a\n", + " label: Rate dependent of rate in-dependent\n", + " widget: Select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: 'Rate dependent '\n", + " disabled: false\n", + " - key: option2\n", + " label: Rate independent\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasModelProperty\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ida13da14f81332\n", + " label: Thermodynamic theory\n", + " widget: Select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: None\n", + " disabled: false\n", + " - key: option2\n", + " label: Rational Thermodynamic\n", + " disabled: false\n", + " - key: option3\n", + " label: Extended Rational Thermodynamic\n", + " disabled: false\n", + " - key: option4\n", + " label: Other thermodynamic theory\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasModelProperty\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: id48ce5b50ef83b\n", + " name: Field of Application\n", + " inputs: []\n", + " hidden: false\n", + " - id: id16ad89c297348\n", + " name: ''\n", + " inputs: []\n", + " hidden: false\n", + " - id: id260478fe428a8\n", + " name: Material parameters\n", + " inputs: []\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-25T11:17:26.603921'\n", + " updated_at: '2025-03-12T23:21:48.982242'\n", + "\n", + "ktype:\n", + " id: magnetic-material\n", + " name: Magnetic material\n", + " webform_schema_id: 3a7d595a-986c-4fba-954f-2ff43caca31b\n", + " webform_schema:\n", + " id: 3a7d595a-986c-4fba-954f-2ff43caca31b\n", + " name: Magnetic material\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/MagneticMaterial\n", + " sections:\n", + " - id: id9d3c2a3a2186c8\n", + " name: Fundamental information\n", + " inputs:\n", + " - id: iddbebcb021cb248\n", + " label: 'Name of the magnetic material '\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " hidden: false\n", + " - id: id2a622413d4c5e\n", + " name: Composition\n", + " inputs:\n", + " - id: idccdea2180ffb78\n", + " label: Compostion (standard notation)\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " hidden: false\n", + " - id: ide01f38b85a8938\n", + " name: Magnetic Properties\n", + " inputs:\n", + " - id: id3511dc5260d8c8\n", + " label: Maximum energy product (BH)_max\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Joule per Cubic Metre\n", + " iri: http://qudt.org/vocab/unit/J-PER-M3\n", + " namespace: http://qudt.org/vocab/unit\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-04-08T15:26:04.858714'\n", + " updated_at: '2025-04-08T15:34:36.094374'\n", + "\n", + "ktype:\n", + " id: srap\n", + " name: Scrap batch\n", + " webform_schema_id: bf25a6e0-ed75-4ab2-b874-2ff02e38e0bd\n", + " webform_schema:\n", + " id: bf25a6e0-ed75-4ab2-b874-2ff02e38e0bd\n", + " name: Scrap batch\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/ScrapBatch\n", + " sections:\n", + " - id: id24bde5f43ae78\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: idc561b2cb686e2\n", + " label: Scrap type\n", + " widget: Multi-select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Aluminium scrap\n", + " disabled: false\n", + " - key: option2\n", + " label: Steel scrap\n", + " disabled: false\n", + " - key: option3\n", + " label: Magnet scrap\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ScrapType\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id2e22f0e9c7cac\n", + " label: Weight\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: kilogram\n", + " iri: http://qudt.org/vocab/unit/KiloGM\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/pmd/co/Weight\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-27T00:11:53.457843'\n", + " updated_at: '2025-03-09T07:44:46.178104'\n", + "\n", + "ktype:\n", + " id: production-process-chain\n", + " name: Production process chain\n", + " webform_schema_id: 7b5e4346-6660-41e9-a066-df6ce9e83144\n", + " webform_schema:\n", + " id: 7b5e4346-6660-41e9-a066-df6ce9e83144\n", + " name: Production process chain\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/ProductionProcessChain\n", + " sections:\n", + " - id: id2e6006d579c25\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: idb9c8b50c13c02\n", + " label: Involved production processes\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: production-process\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-03T09:16:17.824248'\n", + " updated_at: '2025-03-03T09:20:04.500849'\n", + "\n", + "ktype:\n", + " id: engineered-material\n", + " name: Engineered material\n", + " webform_schema_id: 5241d526-156d-481e-8412-cf21e0f5842f\n", + " webform_schema:\n", + " id: 5241d526-156d-481e-8412-cf21e0f5842f\n", + " name: Engineered material\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " sections:\n", + " - id: id94fcf64304dcf8\n", + " name: Untitled\n", + " inputs:\n", + " - id: idc433348a844ea\n", + " label: Material number\n", + " widget: Text\n", + " required: false\n", + " hint: Material number (Werkstoffnummer) after EN 10027-2:1992-09\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id5ec90ef5aca04\n", + " label: Material abbreviation\n", + " widget: Text\n", + " required: false\n", + " hint: e.g. X6CrNiTi18-10\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id90b1a7433f4f28\n", + " label: Minimum Tensile strength\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: N/mm^2\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id991fbd92598258\n", + " label: AISI designation\n", + " widget: Text\n", + " required: false\n", + " hint: Classification after the American Iron and Steel Institute\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id7f0f1cb64a93f8\n", + " label: Recommended fillter metal\n", + " widget: Text\n", + " required: false\n", + " hint: 'material suitable for joint welding, e.g. 4430 '\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id2bc92ba6732978\n", + " label: Maximum annealed core hardness\n", + " widget: Number\n", + " required: false\n", + " hint: HB (Brinell Hardness)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id663bfcfaf22cb\n", + " label: Machining\n", + " widget: Number\n", + " required: false\n", + " hint: 'at an advance of 0,4 mm/r '\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: mm/min\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: ideb3fbffa636fd\n", + " label: Maximum recommended application temperature\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: \"\\xB0C\"\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id2a8a4eaf8476d8\n", + " label: Achievable strength class\n", + " widget: Text\n", + " required: false\n", + " hint: A=S235, B=S275, C=S355, D=S460, E=S690, F = S880\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id2394fdbef80ce\n", + " label: Pressure vessel construction approved\n", + " widget: Checkbox\n", + " required: false\n", + " value: false\n", + " hint: after EN-DIN 10272\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-10-23T12:43:28.801406'\n", + " updated_at: '2024-10-23T13:01:04.742102'\n", + "\n", + "ktype:\n", + " id: characterization-process\n", + " name: Characterization Process\n", + " webform_schema_id: e38cff0d-c7fb-43c3-ad50-383c5e5ecfc0\n", + " webform_schema:\n", + " id: e38cff0d-c7fb-43c3-ad50-383c5e5ecfc0\n", + " name: Characterization Process\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " sections:\n", + " - id: id70b5ab713ca8d\n", + " name: Untitled\n", + " inputs:\n", + " - id: id61800fb15eea48\n", + " label: Start time\n", + " widget: Text\n", + " required: false\n", + " hint: When the process was initalized\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: dd/mm/yyyy\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTimeStamp_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id007382f9957a7\n", + " label: Completion time\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: dd/mm/yyyy\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTimeStamp_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idc7727de56797e\n", + " label: Process description\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasRemark_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id3986828345c24\n", + " label: Name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id8355485bdc96f\n", + " label: Name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id880a6e061da58\n", + " label: Name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id73697ca2c6e228\n", + " label: Identifier\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-11-13T13:39:51.818431'\n", + " updated_at: '2024-11-15T11:40:00.136101'\n", + "\n", + "ktype:\n", + " id: act\n", + " name: Act\n", + " webform_schema_id: b8e559ef-ff1e-460d-85f4-f163322da908\n", + " webform_schema:\n", + " id: b8e559ef-ff1e-460d-85f4-f163322da908\n", + " name: Act\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/Act\n", + " sections:\n", + " - id: id17c04e08e1ce7\n", + " name: ''\n", + " inputs:\n", + " - id: id6082fe47586758\n", + " label: Title of the Act\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#publication\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idf9d59f1110cc18\n", + " label: Date of publication\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#dateOfPublication\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-04-05T15:26:42.185420'\n", + " updated_at: '2025-04-05T15:26:42.185420'\n", + "\n", + "ktype:\n", + " id: production-device\n", + " name: Production device\n", + " webform_schema_id: d1989a12-a382-468d-a033-73b7ee692f09\n", + " webform_schema:\n", + " id: d1989a12-a382-468d-a033-73b7ee692f09\n", + " name: Production device\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo#EMMO_256bb4be_78c6_4f2f_8589_f5e4c8339bbd\n", + " sections:\n", + " - id: idacaf49aabe4d8\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: idb37d85da854f8\n", + " label: Name of the production device\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasProductionDeviceName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id69ad9baba47c\n", + " label: Manufacturer\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo/domain/characterisation-methodology/chameo#hasManufacturer\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id69221805915f5\n", + " label: Year of manufacture\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#dateOfProduction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id2f4fcc96caf08\n", + " label: Production device type\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ProcuctionDeviceType\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id5de744587dc7e\n", + " label: Serial Number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasSerialNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id6c3f63c77fdac\n", + " label: Responsible organization for the device\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasResponsibleOrganizationUnit\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_c0f72631_d7c2_434c_9c26_5c44123df682\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - organization\n", + " - id: id0cb4a7a37833e\n", + " label: Room number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasRoomNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: idc9875a15a8b2d\n", + " label: Inventory number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasInventoryNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: idc20ea1cef3cb4\n", + " label: Contact person\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_1246b120_abbe_4840_b0f8_3e4348b24a17\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - expert\n", + " - id: idfa6adf1c18178\n", + " label: Operation range\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/OperationRange\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-26T18:15:41.787658'\n", + " updated_at: '2025-04-10T00:20:37.744549'\n", + "\n", + "ktype:\n", + " id: emmc-workshop\n", + " name: EMMC-Workshop\n", + " webform_schema_id: efb66b2f-1d58-43eb-b733-f18ae8ecf507\n", + " webform_schema:\n", + " id: efb66b2f-1d58-43eb-b733-f18ae8ecf507\n", + " name: EMMC-Workshop\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/EMMC-Workshop\n", + " sections:\n", + " - id: id48cdfd3a4b8fc8\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: idb524743c8bbab8\n", + " label: Year\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDeliveryDate\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-04-10T08:43:14.129840'\n", + " updated_at: '2025-04-10T08:43:59.914287'\n", + "\n", + "ktype:\n", + " id: silver-palladium-alloy\n", + " name: Silver Palladium Alloy\n", + " webform_schema_id: fdea16d4-7b8c-4ad9-9def-53f0b1120958\n", + " webform_schema:\n", + " id: fdea16d4-7b8c-4ad9-9def-53f0b1120958\n", + " name: Silver Palladium Alloy\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/pmd/co/ChemicalComposition\n", + " sections:\n", + " - id: idf65262631f6ee\n", + " name: Composition\n", + " inputs:\n", + " - id: id845324b7b61f8\n", + " label: 'Silver '\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasSilverMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idb0b164af3d40c\n", + " label: Palladium\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasPalladiumMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: idcd5897611d475\n", + " name: Physical properties\n", + " inputs:\n", + " - id: id7f9b735518b03\n", + " label: Density\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Gram Per Cubic Centimetre\n", + " iri: http://qudt.org/vocab/unit/GM-PER-CentiM3\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/steel/ProcessOntology/MassDensity\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id55fb4d9662696\n", + " label: Melting point of range\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: degree Celsius\n", + " iri: http://qudt.org/vocab/unit/DEG_C\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/pmd/co/Temperature\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: -273\n", + " max: 2000\n", + " step: 0.1\n", + " range: false\n", + " hidden: false\n", + " - id: id702cad61a7a59\n", + " name: Mechanical properties\n", + " inputs:\n", + " - id: id7edac774912e5\n", + " label: Tensile Strength R_m\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Megapascal\n", + " iri: http://qudt.org/vocab/unit/MegaPA\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_5d73661e_e710_4844_ab9b_a85b7e68576a\n", + " type: object_property\n", + " class_iri: https://w3id.org/steel/ProcessOntology/TensileStrength\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-14T13:59:00.319704'\n", + " updated_at: '2025-03-14T17:12:03.414629'\n", + "\n", + "ktype:\n", + " id: raw-material\n", + " name: Raw material\n", + " webform_schema_id: d74b4e3a-a514-4858-aba7-6dc9963eb1b7\n", + " webform_schema:\n", + " id: d74b4e3a-a514-4858-aba7-6dc9963eb1b7\n", + " name: Raw material\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " sections:\n", + " - id: id2297e7c70a55e\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id7568839cf603d\n", + " label: Name of the raw material\n", + " widget: Text\n", + " required: false\n", + " hint: 'Add the name of the raw material: e.g. iron ore'\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id82d4e002211d4\n", + " label: Masse\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Tonne\n", + " iri: http://qudt.org/vocab/unit/TONNE\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDefaultValue_Object\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_ed4af7ae_63a2_497e_bb88_2309619ea405\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-25T16:00:50.596497'\n", + " updated_at: '2025-02-26T23:11:39.497733'\n", + "\n", + "ktype:\n", + " id: project\n", + " name: Project\n", + " webform_schema_id: eb4fbf77-dc45-4b35-96e0-e12dc9858a88\n", + " webform_schema:\n", + " id: eb4fbf77-dc45-4b35-96e0-e12dc9858a88\n", + " name: Project\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/pmd/co/Project\n", + " sections:\n", + " - id: id4cdf930805703\n", + " name: Fundamental information\n", + " inputs:\n", + " - id: id1475863a6734b\n", + " label: Project short name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasProjectName_Object\n", + " type: object_property\n", + " class_iri: https://www.materials.fraunhofer.de/ontologies/BWMD_ontology/domain#BWMD_00394\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id0e2567e83ee82\n", + " label: Project name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasProjectName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idb13d750ebf668\n", + " label: Organisation\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/org#organization\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: organization\n", + " - id: idda331a2c485b6\n", + " label: Project number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasProjectNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " placeholder: Please add the project number\n", + " - id: idad6f75e0569f8\n", + " label: Project members\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/agrelon#isMemberOf\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: expert\n", + " - id: idf533e1a7c76d8\n", + " label: Webpage of the project\n", + " widget: Text\n", + " required: false\n", + " hint: Please fill the http/https address of the webpage\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_ac852bf0_3251_4d6b_9e57_acbfcb5e7e08\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: id1b9da0f945fda\n", + " name: Project related data\n", + " inputs:\n", + " - id: ida9b609cef97dd\n", + " label: Dataset\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/related\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: dataset\n", + " - id: idf5a031991b475\n", + " label: Dataset catalog\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/related\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: dataset-catalog\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-25T11:37:41.262256'\n", + " updated_at: '2025-01-25T16:12:32.079754'\n", + "\n", + "ktype:\n", + " id: document\n", + " name: Document\n", + " webform_schema_id: 1a63e770-591c-4a2f-b1ec-76378692cb3a\n", + " webform_schema:\n", + " id: 1a63e770-591c-4a2f-b1ec-76378692cb3a\n", + " name: Document\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/pmd/co/Document\n", + " sections:\n", + " - id: id38dbc9d1053ad\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id81f0f0ebb5e3a\n", + " label: Publication title\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the title of the publication\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#publication\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id03034c5f9a788\n", + " label: Author or Authors\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the author in the form last name, first name, use semicolon\n", + " as separator\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#author\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: idd2286b953224c\n", + " label: Date of Publication\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#dateOfPublication\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: idcc2169d761d01\n", + " label: Keyword or Keywords\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the keyword, separated by semicolons\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#keyword\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id8889f147b9d65\n", + " label: Link to the source of the document\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#downloadURL\n", + " type: object_property\n", + " class_iri: http://purl.org/spar/datacite/ResourceIdentifier\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type:\n", + " - null\n", + " - id: id9141909089bd08\n", + " label: Type of Publication\n", + " widget: Select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Article\n", + " disabled: false\n", + " - key: option2\n", + " label: Book\n", + " disabled: false\n", + " - key: option3\n", + " label: Standard\n", + " disabled: false\n", + " - key: option4\n", + " label: Study\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasTypeOfPublication\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-26T10:25:35.097011'\n", + " updated_at: '2025-05-01T11:00:25.897314'\n", + "\n", + "ktype:\n", + " id: web-ressource\n", + " name: Web ressource\n", + " webform_schema_id: 2527c264-90d3-48bb-88ff-3837239024d7\n", + " webform_schema:\n", + " id: 2527c264-90d3-48bb-88ff-3837239024d7\n", + " name: Web ressource\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://schema.org/WebContent\n", + " sections:\n", + " - id: ida607b61c906a78\n", + " name: ''\n", + " inputs:\n", + " - id: idc605489032d46\n", + " label: Link to the web ressource\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the http or https address\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/ressourceUri\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-05-18T08:23:06.240726'\n", + " updated_at: '2025-05-18T08:23:06.240726'\n", + "\n", + "ktype:\n", + " id: measurement-process\n", + " name: Measurement process\n", + " webform_schema_id: 0097cafa-4a2d-4070-aebf-26111b047ca1\n", + " webform_schema:\n", + " id: 0097cafa-4a2d-4070-aebf-26111b047ca1\n", + " name: Measurement process\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationMeasurementProcess\n", + " sections:\n", + " - id: id034171a74270c\n", + " name: Basic information\n", + " inputs:\n", + " - id: idc229c5d518d05\n", + " label: Type of the Measurement Process\n", + " widget: Multi-select\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Tensile test\n", + " disabled: false\n", + " - key: option2\n", + " label: Shear test\n", + " disabled: false\n", + " - key: option3\n", + " label: Compression test\n", + " disabled: false\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idb7e4a5907b9b6\n", + " label: Operator\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasOperator\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: expert\n", + " hidden: false\n", + " - id: id3354e00cd7562\n", + " name: Specimen\n", + " inputs:\n", + " - id: id65d6ab413dadf\n", + " label: Specimen type\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: iddded49bc61745\n", + " label: 'Specimen '\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasInput\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: specimen\n", + " hidden: false\n", + " - id: id1f25c686b9c18\n", + " name: Measurement data\n", + " inputs:\n", + " - id: id6ead211a496bd\n", + " label: Dataset\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: annotation_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: dataset\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-07T17:57:43.973453'\n", + " updated_at: '2025-03-18T20:14:47.520674'\n", + "\n", + "ktype:\n", + " id: standard\n", + " name: Standard\n", + " webform_schema_id: 9fc0926e-d672-46cb-8939-011a1f6c0243\n", + " webform_schema:\n", + " id: 9fc0926e-d672-46cb-8939-011a1f6c0243\n", + " name: Standard\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/steel/ProcessOntology/Standard\n", + " sections:\n", + " - id: id42e0b41b4b41a8\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id0e43b10a181d\n", + " label: Title of the standard\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://purl.org/dc/terms/title\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-18T20:30:18.372742'\n", + " updated_at: '2025-03-18T20:57:36.405719'\n", + "\n", + "ktype:\n", + " id: external-data-source\n", + " name: Data source\n", + " webform_schema_id: b07f0caa-302f-4738-826b-072325c2df51\n", + " webform_schema:\n", + " id: b07f0caa-302f-4738-826b-072325c2df51\n", + " name: Data source\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - http://purl.org/spar/datacite/ResourceIdentifier\n", + " sections:\n", + " - id: ida97cb1490e7cd\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id1218ca74779d5\n", + " label: Name of the data source\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasDataSocurceName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ide3c0cc34a59d\n", + " label: Link to the data source\n", + " widget: Text\n", + " required: false\n", + " hint: http/https-link to the data source\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#downloadURL\n", + " type: object_property\n", + " class_iri: http://purl.org/spar/datacite/ResourceIdentifier\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: ida14fe37d66557\n", + " name: Data source integration information\n", + " inputs:\n", + " - id: idcd41e2dd7d6c6\n", + " label: Semantic integration level\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/SemanticIntegrationLevelDataSource\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-26T11:57:31.456954'\n", + " updated_at: '2025-03-22T10:48:30.450920'\n", + "\n", + "ktype:\n", + " id: aluminium-alloy\n", + " name: Aluminium Alloy\n", + " webform_schema_id: dbb2ab1d-dc2a-47d9-aa4f-09475b0270f1\n", + " webform_schema:\n", + " id: dbb2ab1d-dc2a-47d9-aa4f-09475b0270f1\n", + " name: Aluminium Alloy\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/pmd/co/EngineeredMaterial\n", + " sections:\n", + " - id: id0c88760a11b16\n", + " name: Composition\n", + " inputs:\n", + " - id: id858e64bc18474\n", + " label: Aluminium mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasAluminiumMassFraction\n", + " type: object_property\n", + " class_iri: https://w3id.org/ORCHESTER/MassFraction\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idce87731554ab8\n", + " label: Copper mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasCopperMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idee3b37dc16d33\n", + " label: Silicium mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasSiliciumMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idad3a991ed01fe\n", + " label: Maganese mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasManganeseMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id0879e3ac279e\n", + " label: Nickel mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasNickelMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ida760eddfc0351\n", + " label: Iron mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasIronMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idf92a135418693\n", + " label: Zirconium mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasZirconiumMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ide0617f19494\n", + " label: Magnesium mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasMagnesiumMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id1d8fc04d2805e\n", + " label: Titanium mass fraction\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Percent\n", + " iri: http://qudt.org/vocab/unit/PERCENT\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasTitaniumMassFraction\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: id6c71facdbfa1b\n", + " name: Mechanical properties\n", + " inputs:\n", + " - id: id547326648372c\n", + " label: Yield stress\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Megapascal\n", + " iri: http://qudt.org/vocab/unit/MegaPA\n", + " namespace: http://qudt.org/vocab/unit\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasYieldStress_MegaPA\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: id5a29d7d611e43\n", + " name: State\n", + " inputs:\n", + " - id: id7679dc67608e6\n", + " label: State of the material\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasState\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-05T23:03:20.969312'\n", + " updated_at: '2025-02-07T08:34:49.108582'\n", + "\n", + "ktype:\n", + " id: carbonfootprint\n", + " name: Carbon Footprint\n", + " webform_schema_id: 93fafda2-4100-48ee-9b04-89e41c4b48e8\n", + " webform_schema:\n", + " id: 93fafda2-4100-48ee-9b04-89e41c4b48e8\n", + " name: Carbon Footprint\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/CarbonFootprint\n", + " sections:\n", + " - id: id55787634bf1a1\n", + " name: Product carbon footprint\n", + " inputs:\n", + " - id: idac53f04f4d77b\n", + " label: Impact assessment method or calculation method\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ImpactAssessmentMethod\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ida52ed70b6b1b4\n", + " label: CO2 equivalent climate change\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HasCO2eValue\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id96f4f0c818676\n", + " label: Reference value for calculation\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ReferenceValueForCalculation\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id459c57e31e284\n", + " label: Quantity of measure for calculation\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/QuantityOfMeasureForCalculation\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idddc69d02e5a2f\n", + " label: Life cycle phase\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/LifeCyclePhase\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id340968d815474\n", + " label: Publication date\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/PublicationDate\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idc926242f5d4d1\n", + " label: Expiration date\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ExpirationDate\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: iddb37353448a71\n", + " label: Explanatory statement\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ExplanatoryStatement\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: document\n", + " hidden: false\n", + " - id: id2dfc950f670c5\n", + " name: Transport carbon footprint\n", + " inputs: []\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-22T19:06:11.380008'\n", + " updated_at: '2025-03-23T07:44:35.790543'\n", + "\n", + "ktype:\n", + " id: address\n", + " name: Address\n", + " webform_schema_id: e736c6f5-165f-4f35-b971-d75ff12abfb2\n", + " webform_schema:\n", + " id: e736c6f5-165f-4f35-b971-d75ff12abfb2\n", + " name: Address\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/pmd/co/Address\n", + " sections:\n", + " - id: id6530fdb5d31f7\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id1fc65a963d12\n", + " label: Street\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/Street\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idc14c0177647d7\n", + " label: House number\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/HouseNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id7d2dfe0c5f82a\n", + " label: Zip code\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/ZipCode\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id2bf819fc77408\n", + " label: City\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/City\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idb6157ed71489\n", + " label: Country\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/Country\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-03-23T07:07:37.893283'\n", + " updated_at: '2025-03-23T07:14:15.200209'\n", + "\n", + "ktype:\n", + " id: manufacturing-process\n", + " name: Manufacturing Process\n", + " webform_schema_id: 4411fe95-b5a9-49a6-a7a2-5d231469bd64\n", + " webform_schema:\n", + " id: 4411fe95-b5a9-49a6-a7a2-5d231469bd64\n", + " name: Manufacturing process\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " sections:\n", + " - id: idc31851effc76f\n", + " name: ''\n", + " inputs:\n", + " - id: id9e62c8f05fa6e8\n", + " label: Process name\n", + " widget: Text\n", + " required: false\n", + " hint: Name of the manufacturing process (e.g., Hot Rolling)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id5ff5607f7d43f\n", + " label: Process Description\n", + " widget: Text\n", + " required: false\n", + " hint: Brief description of the process\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id4804cada693ad8\n", + " label: Process duration\n", + " widget: Number\n", + " required: false\n", + " hint: Typical duration of the process\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: min\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTime_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id2b41376167e7f\n", + " label: Temperature\n", + " widget: Number\n", + " required: false\n", + " hint: Operating temperature (if applicable)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: DegC\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id700c62fd038a58\n", + " label: Cost\n", + " widget: Number\n", + " required: false\n", + " hint: Estimated cost of the process\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: Euro/min\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-10-23T13:50:27.891897'\n", + " updated_at: '2026-05-03T14:54:46.818981'\n", + "\n", + "ktype:\n", + " id: expert\n", + " name: Expert\n", + " webform_schema_id: 3c4d676d-03a7-40d6-8c1b-4a0b9b56a5d9\n", + " webform_schema:\n", + " id: 3c4d676d-03a7-40d6-8c1b-4a0b9b56a5d9\n", + " name: expert\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: true\n", + " class_mapping:\n", + " - https://d-nb.info/standards/elementset/gnd#Person\n", + " sections:\n", + " - id: id01d808779f18d\n", + " name: General information about the Expert\n", + " inputs:\n", + " - id: id73014ceccdf35\n", + " label: First Name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/firstName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: ide3e3058da9784\n", + " label: Last name\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/lastName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id220e33fbd6ce1\n", + " label: Employer\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/agrelon#hasEmployer\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_c0f72631_d7c2_434c_9c26_5c44123df682\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: organization\n", + " - id: id4360d4bc4ab02\n", + " label: Department\n", + " widget: Knowledge item\n", + " required: false\n", + " hint: 'Select the '\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/org#memberOf\n", + " type: property\n", + " class_iri: http://www.w3.org/ns/org#OrganizationalUnit\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: organization\n", + " - id: id693ce596955fb\n", + " label: Experts you know\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/knows\n", + " type: object_property\n", + " class_iri: https://d-nb.info/standards/elementset/gnd#Person\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: expert\n", + " hidden: false\n", + " - id: id9ad5da56ad5dd\n", + " name: Expertise\n", + " inputs:\n", + " - id: idc90df1ddeb336\n", + " label: Expertise in materials\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#topic\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: engineered-material\n", + " - id: id7aba01bb91426\n", + " label: Expertise in material modelling\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#topic\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: material-model\n", + " - id: id47391ebd4d64\n", + " label: Expertise in measurement devices\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasMeasurementDeviceName\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: true\n", + " knowledge_type: measurement-device\n", + " hidden: false\n", + " - id: id69dc30cd5bd52\n", + " name: Education\n", + " inputs:\n", + " - id: id2fe41d8056068\n", + " label: Education or Field of Study\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://d-nb.info/standards/elementset/gnd#fieldOfStudy\n", + " type: object_property\n", + " class_iri: https://d-nb.info/standards/elementset/gnd#Gods\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-07-28T18:48:21.024154'\n", + " updated_at: '2026-05-11T12:12:16.458478'\n", + "\n", + "ktype:\n", + " id: semi-finished-product\n", + " name: Semi-finished product\n", + " webform_schema_id: 224297da-9a8f-4db7-af8f-37dcff8a4bbd\n", + " webform_schema:\n", + " id: 224297da-9a8f-4db7-af8f-37dcff8a4bbd\n", + " name: Semi-finished product\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " sections:\n", + " - id: id2f5555b5b6faf\n", + " name: ''\n", + " inputs:\n", + " - id: ida071d231324aa8\n", + " label: Product name\n", + " widget: Text\n", + " required: false\n", + " hint: Name of the semi-finished product (e.g. Slab)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasIdentifier_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id8c02df4e44758\n", + " label: Product Description\n", + " widget: Textarea\n", + " required: false\n", + " hint: Brief description of the product\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idf272d26c87f7c8\n", + " label: Width\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: mm\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasWidth_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idcc11a8330cfb88\n", + " label: Length\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: mm\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTotalLength_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idfda8888c1ce61\n", + " label: Thickness\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: mm\n", + " namespace: None\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasOriginalThickness_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: id1959ef37f8cb2\n", + " label: Weigth\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " iri: None\n", + " symbol: g\n", + " namespace: None\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idd62839912aa2c\n", + " label: Surface finish\n", + " widget: Select\n", + " required: false\n", + " value: ''\n", + " hint: Type of surface finish (e.g., rough, smooth)\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Rough\n", + " disabled: false\n", + " - key: option2\n", + " label: Smooth\n", + " disabled: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " - id: idc53d9915e39df8\n", + " label: Material state\n", + " widget: Select\n", + " required: false\n", + " value: ''\n", + " hidden: false\n", + " ignore: false\n", + " select_options:\n", + " - key: option1\n", + " label: Raw\n", + " disabled: false\n", + " - key: option2\n", + " label: Rolled\n", + " disabled: false\n", + " - key: option3\n", + " label: Cut\n", + " disabled: false\n", + " - key: option4\n", + " label: Heat Treated\n", + " disabled: false\n", + " - key: option5\n", + " label: Annealed\n", + " disabled: false\n", + " - key: option6\n", + " label: Quenched\n", + " disabled: false\n", + " - key: option7\n", + " label: Tempered\n", + " disabled: false\n", + " - key: option8\n", + " label: Assembled\n", + " disabled: false\n", + " - key: option9\n", + " label: Machined\n", + " disabled: false\n", + " - key: option10\n", + " label: Fabricated\n", + " disabled: false\n", + " - key: option11\n", + " label: Recycled\n", + " disabled: false\n", + " - key: option12\n", + " label: Blanked\n", + " disabled: false\n", + " - key: option13\n", + " label: Finished\n", + " disabled: false\n", + " multiple_selection: false\n", + " range_options:\n", + " min: 0\n", + " max: 0\n", + " step: 0\n", + " range: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-10-23T13:54:07.583405'\n", + " updated_at: '2024-11-15T11:30:49.341861'\n", + "\n", + "ktype:\n", + " id: raw-material-profile\n", + " name: Raw material profile\n", + " webform_schema_id: 6e116711-2aff-4efb-960b-7a2c6540985e\n", + " webform_schema:\n", + " id: 6e116711-2aff-4efb-960b-7a2c6540985e\n", + " name: Raw material profile\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " sections:\n", + " - id: idecd132593127b\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: idd5569135a88cb\n", + " label: Raw material (name of the chemical element)\n", + " widget: Knowledge item\n", + " required: false\n", + " hint: Please add the name of the chemical element\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_79c0edfa_06f9_5149_b754_28c589035b8a\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: chemical-element\n", + " - id: ida85038c9ab6d3\n", + " label: RMIS source\n", + " widget: Text\n", + " required: false\n", + " hint: https-link to the RMIS Source\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/emmo#EMMO_ac852bf0_3251_4d6b_9e57_acbfcb5e7e08\n", + " type: property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-01-26T11:16:03.599447'\n", + " updated_at: '2025-01-26T11:31:06.709956'\n", + "\n", + "ktype:\n", + " id: tensile-test\n", + " name: Tensile Test\n", + " created_at: '2026-05-03T14:40:51.935637'\n", + " updated_at: '2026-05-03T14:40:51.935637'\n", + "\n", + "ktype:\n", + " id: organization\n", + " name: Organization\n", + " webform_schema_id: 9a110d95-49fb-4c8f-8565-52a4848e3361\n", + " webform_schema:\n", + " id: 9a110d95-49fb-4c8f-8565-52a4848e3361\n", + " name: organization\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo#EMMO_c0f72631_d7c2_434c_9c26_5c44123df682\n", + " sections:\n", + " - id: id21866f42d5176\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id4d61120f8538b\n", + " label: Name of the Organisation\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id2af8ad6c4bb5a\n", + " label: Sub Organisation of\n", + " widget: Knowledge item\n", + " required: false\n", + " hint: Please select the upper organisation\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/org#subOrganizationOf\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: organization\n", + " - id: id5880a22a80ecf\n", + " label: URL of the webpage\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#accessURL\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3e32c796f00c7\n", + " label: City\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id5597c3fd52603\n", + " label: Address\n", + " widget: Knowledge item\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: address\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2024-07-28T18:48:21.024154'\n", + " updated_at: '2026-05-03T14:50:20.049632'\n", + "\n", + "ktype:\n", + " id: material\n", + " name: Material\n", + " created_at: '2026-05-03T14:50:25.990175'\n", + " updated_at: '2026-05-03T14:50:25.990175'\n", + "\n", + "ktype:\n", + " id: dasda\n", + " name: dasda\n", + " created_at: '2026-05-03T15:18:30.461044'\n", + " updated_at: '2026-05-03T15:18:30.461044'\n", + "\n", + "ktype:\n", + " id: process\n", + " name: Process\n", + " created_at: '2026-05-03T15:36:18.563875'\n", + " updated_at: '2026-05-03T15:36:18.563875'\n", + "\n", + "ktype:\n", + " id: chemical-composition\n", + " name: Chemical Composition\n", + " created_at: '2026-05-03T16:34:37.231511'\n", + " updated_at: '2026-05-03T16:34:37.231511'\n", + "\n", + "ktype:\n", + " id: measurement-device\n", + " name: Measurement Device\n", + " webform_schema_id: ab1ab1d5-10c6-4096-a99b-3e6fb43478ea\n", + " webform_schema:\n", + " id: ab1ab1d5-10c6-4096-a99b-3e6fb43478ea\n", + " name: Measurement device\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/MeasurementDevice\n", + " sections:\n", + " - id: id23f26e1aec6e\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id4771ba047219e\n", + " label: Name of measurement device\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the name of the measurement device\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasMeasurementDeviceName\n", + " type: data_property\n", + " class_iri: https://w3id.org/ORCHESTER/measurement-device/MeasurementDeviceName\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3d095d631a156\n", + " label: Manufacturer\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the name of the manufacturer\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasManufacturer\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id4584da4ce4bc9\n", + " label: Measurement device type\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the measurement type of the device, e.g. microscope\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/MeasurementDeviceType\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id071732bc59fed\n", + " label: Serial number\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the serial number\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasSerialNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id7550e1c6439dd\n", + " label: Responsible organization for the device\n", + " widget: Knowledge item\n", + " required: false\n", + " hint: Please add the responsible department of the organisation\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasResponsibleOrganizationUnit\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_c0f72631_d7c2_434c_9c26_5c44123df682\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: organization\n", + " - id: id2095e14118a78\n", + " label: Room number\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the room number of the device\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasRoomNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idcce5f8dfb7778\n", + " label: Inventory number\n", + " widget: Text\n", + " required: false\n", + " hint: please add the sigma inventory number\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasInventoryNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3998befe78c2c\n", + " label: Measurement range\n", + " widget: Textarea\n", + " required: false\n", + " hint: Please add relevant information about the measurement range\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasMeasurementRange\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-17T11:22:02.248650'\n", + " updated_at: '2026-05-25T15:17:12.862130'\n", + "\n", + "ktype:\n", + " id: material-card\n", + " name: Material Card\n", + " created_at: '2026-05-25T15:36:20.811768'\n", + " updated_at: '2026-05-25T15:36:20.811768'\n", + "\n", + "ktype:\n", + " id: person\n", + " name: Person\n", + " created_at: '2026-05-25T16:35:22.337537'\n", + " updated_at: '2026-05-25T16:35:22.337537'\n", + "\n", + "ktype:\n", + " id: creep-specimen\n", + " name: Creep Specimen\n", + " webform_schema_id: 195680a9-25d0-4630-b160-f412a31725d5\n", + " webform_schema:\n", + " id: 195680a9-25d0-4630-b160-f412a31725d5\n", + " name: Creep specimen\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: http://purl.obolibrary.org/obo/OBI_0100051\n", + " sections:\n", + " - id: section-creep-gauge\n", + " name: Gauge Section\n", + " inputs:\n", + " - id: input-creep-gauge-length\n", + " label: Gauge length\n", + " widget: Number\n", + " required: false\n", + " hint: Length of the parallel gauge section (L0)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasGaugeLengthLo_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-creep-gauge-diameter\n", + " label: Gauge diameter\n", + " widget: Number\n", + " required: false\n", + " hint: Diameter of the cylindrical gauge section (for round specimens)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDiameter_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " - id: section-creep-grip\n", + " name: Grip Ends\n", + " inputs:\n", + " - id: input-creep-grip-type\n", + " label: Grip type\n", + " widget: Select\n", + " required: false\n", + " hint: How the specimen is held in the testing machine\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleType_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-creep-thread-spec\n", + " label: Thread specification\n", + " widget: Text\n", + " required: false\n", + " hint: \"e.g. M10 \\xD7 1.5, 3/8-24 UNF\"\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2026-06-04T16:45:11.535394'\n", + " created_at: '2025-04-10T17:59:37.038081'\n", + " updated_at: '2026-06-03T14:30:12.659529'\n", + "\n", + "ktype:\n", + " id: flat-specimen\n", + " name: Flat Specimen\n", + " webform_schema_id: a6bad5c1-6c0c-4d7d-81a9-342df6c0bc53\n", + " webform_schema:\n", + " id: a6bad5c1-6c0c-4d7d-81a9-342df6c0bc53\n", + " name: flat-specimen_custom_properties\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: https://w3id.org/pmd/co/Specimen\n", + " sections:\n", + " - id: section-flat-gauge\n", + " name: Gauge Section\n", + " inputs:\n", + " - id: input-flat-gauge-length\n", + " label: Gauge length\n", + " widget: Number\n", + " required: false\n", + " hint: Parallel section length over which strain is measured (L0)\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasGaugeLengthLo_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-flat-gauge-width\n", + " label: Gauge width\n", + " widget: Number\n", + " required: false\n", + " hint: Width within the parallel gauge section\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasParallelSectionWidth_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-flat-shoulder-radius\n", + " label: Shoulder radius\n", + " widget: Number\n", + " required: false\n", + " hint: Transition radius between gauge section and grip area\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasRadius_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2026-06-03T18:19:43.815570'\n", + " updated_at: '2026-06-03T18:37:53.450387'\n", + " created_at: '2026-06-03T14:32:17.816061'\n", + " updated_at: '2026-06-03T18:19:43.815570'\n", + "\n", + "ktype:\n", + " id: metal-sheet-batch\n", + " name: Metal Sheet Batch\n", + " created_at: '2026-06-04T13:36:01.424615'\n", + " updated_at: '2026-06-04T13:36:01.424615'\n", + "\n", + "ktype:\n", + " id: specimen-batch\n", + " name: Specimen Batch\n", + " created_at: '2026-06-04T13:56:07.018955'\n", + " updated_at: '2026-06-04T13:56:07.018955'\n", + "\n", + "ktype:\n", + " id: batch\n", + " name: Batch\n", + " webform_schema_id: ff2017ab-4371-40dd-ae54-6cc806e660b2\n", + " webform_schema:\n", + " id: ff2017ab-4371-40dd-ae54-6cc806e660b2\n", + " name: batch_custom_properties\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: http://www.w3.org/ns/prov#Collection\n", + " sections:\n", + " - id: section-batch-info\n", + " name: Batch Information\n", + " inputs:\n", + " - id: input-batch-id\n", + " label: Batch ID\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://schema.org/identifier\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-batch-date\n", + " label: Production / receipt date\n", + " widget: Date\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://schema.org/dateCreated\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-batch-quantity\n", + " label: Quantity\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://schema.org/numberOfItems\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2026-06-04T16:12:01.564921'\n", + " updated_at: '2026-06-04T16:34:06.838525'\n", + " created_at: '2026-06-03T15:03:46.122464'\n", + " updated_at: '2026-06-04T16:12:01.564921'\n", + "\n", + "ktype:\n", + " id: metal-sheet\n", + " name: Metal Sheet\n", + " webform_schema_id: ca38d31b-d35a-4880-804e-1c3b138b159e\n", + " webform_schema:\n", + " id: ca38d31b-d35a-4880-804e-1c3b138b159e\n", + " name: metal-sheet_custom_properties\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: []\n", + " sections:\n", + " - id: section-sheet-dimensions\n", + " name: Dimensions\n", + " inputs:\n", + " - id: input-sheet-thickness\n", + " label: Thickness\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceThickness_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-sheet-width\n", + " label: Width\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasWidth_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-sheet-length\n", + " label: Length\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " measurement_unit:\n", + " label: Millimetre\n", + " iri: http://qudt.org/vocab/unit/MilliM\n", + " symbol: mm\n", + " namespace: qudt\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceLength_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-sheet-surface\n", + " label: Surface condition\n", + " widget: Text\n", + " required: false\n", + " hint: e.g. hot-rolled, cold-rolled, annealed, pickled\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2026-06-04T17:01:11.181898'\n", + " updated_at: '2026-06-04T17:01:11.181898'\n", + " created_at: '2026-06-04T17:01:11.181898'\n", + " updated_at: '2026-06-04T17:01:11.181898'\n", "\n" ] } @@ -583,7 +3915,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/2_creation.ipynb b/docs/dsms_sdk/tutorials/2_creation.ipynb index eb9f035..7711a2b 100644 --- a/docs/dsms_sdk/tutorials/2_creation.ipynb +++ b/docs/dsms_sdk/tutorials/2_creation.ipynb @@ -21,8 +21,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:01.020763Z", + "iopub.status.busy": "2026-06-07T20:52:01.020541Z", + "iopub.status.idle": "2026-06-07T20:52:01.687641Z", + "shell.execute_reply": "2026-06-07T20:52:01.686445Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem" @@ -37,8 +44,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:01.690742Z", + "iopub.status.busy": "2026-06-07T20:52:01.690406Z", + "iopub.status.idle": "2026-06-07T20:52:02.203284Z", + "shell.execute_reply": "2026-06-07T20:52:02.202115Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -62,11 +76,69 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:02.206918Z", + "iopub.status.busy": "2026-06-07T20:52:02.206739Z", + "iopub.status.idle": "2026-06-07T20:52:02.438543Z", + "shell.execute_reply": "2026-06-07T20:52:02.437328Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:430: UserWarning: A flat dictionary was provided for custom properties.\n", + " Will be transformed into `KItemCustomPropertiesModel`.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "kitem:\n", + " name: Specimen123\n", + " ktype_id: specimen\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "item = KItem(\n name=\"Specimen123\",\n ktype_id=dsms.ktypes.Specimen,\n custom_properties = {\n \"Width\": 0.5,\n \"Length\": 0.15,\n }\n)\n\nitem" + "item = KItem(\n", + " name=\"Specimen123\",\n", + " ktype_id=dsms.ktypes.Specimen,\n", + " custom_properties = {\n", + " \"Width\": 0.5,\n", + " \"Length\": 0.15,\n", + " }\n", + ")\n", + "\n", + "item" ] }, { @@ -78,9 +150,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:02.444718Z", + "iopub.status.busy": "2026-06-07T20:52:02.444547Z", + "iopub.status.idle": "2026-06-07T20:52:04.586941Z", + "shell.execute_reply": "2026-06-07T20:52:04.586003Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'https://nash.materials-data.space/knowledge/specimen/specimen123-227d1678'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "dsms.add(item)\n", "dsms.commit()\n", @@ -96,9 +186,71 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.588944Z", + "iopub.status.busy": "2026-06-07T20:52:04.588786Z", + "iopub.status.idle": "2026-06-07T20:52:04.594447Z", + "shell.execute_reply": "2026-06-07T20:52:04.593683Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 227d1678-aa56-4598-9082-540c629fd8a8\n", + " name: Specimen123\n", + " ktype_id: specimen\n", + " slug: specimen123-227d1678\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:02.878207\n", + " updated_at: 2026-06-07 20:52:02.878207\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item" ] @@ -112,9 +264,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.596101Z", + "iopub.status.busy": "2026-06-07T20:52:04.595959Z", + "iopub.status.idle": "2026-06-07T20:52:04.599363Z", + "shell.execute_reply": "2026-06-07T20:52:04.598541Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Specimen123'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.name" ] @@ -128,9 +298,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.600769Z", + "iopub.status.busy": "2026-06-07T20:52:04.600633Z", + "iopub.status.idle": "2026-06-07T20:52:04.603939Z", + "shell.execute_reply": "2026-06-07T20:52:04.603141Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "UUID('227d1678-aa56-4598-9082-540c629fd8a8')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.id" ] @@ -144,9 +332,131 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.605534Z", + "iopub.status.busy": "2026-06-07T20:52:04.605391Z", + "iopub.status.idle": "2026-06-07T20:52:04.612764Z", + "shell.execute_reply": "2026-06-07T20:52:04.611929Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ktype:\n", + " id: specimen\n", + " name: Specimen\n", + " webform_schema_id: 7199e339-2512-4aed-8b99-af495c0349d3\n", + " webform_schema:\n", + " id: 7199e339-2512-4aed-8b99-af495c0349d3\n", + " name: Specimen\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping: http://purl.obolibrary.org/obo/OBI_0100051\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " inputs:\n", + " - id: input-specimen-type\n", + " label: Specimen type\n", + " widget: Text\n", + " required: false\n", + " hint: e.g. flat, round, notched\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleType_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-geometry\n", + " label: Specimen geometry\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceGeometry_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-width\n", + " label: Width\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasWidth_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-length\n", + " label: Length\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceLength_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-thickness\n", + " label: Thickness\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasTestPieceThickness_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-diameter\n", + " label: Diameter\n", + " widget: Number\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDiameter_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: input-specimen-description\n", + " label: Additional description\n", + " widget: Textarea\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2026-06-04T16:44:35.090388'\n", + " created_at: '2024-10-23T13:59:30.448842'\n", + " updated_at: '2024-11-15T11:28:54.289602'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.ktype" ] @@ -160,9 +470,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.614193Z", + "iopub.status.busy": "2026-06-07T20:52:04.614056Z", + "iopub.status.idle": "2026-06-07T20:52:04.617401Z", + "shell.execute_reply": "2026-06-07T20:52:04.616627Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.is_a(dsms.ktypes.Specimen)" ] @@ -176,9 +504,25 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.618989Z", + "iopub.status.busy": "2026-06-07T20:52:04.618855Z", + "iopub.status.idle": "2026-06-07T20:52:04.707770Z", + "shell.execute_reply": "2026-06-07T20:52:04.706752Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: RDF subgraph is generated asynchronously.\n", + "It may not be available immediately after creation.\n" + ] + } + ], "source": [ "try:\n", " print(item.subgraph.serialize())\n", @@ -196,9 +540,25 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.709914Z", + "iopub.status.busy": "2026-06-07T20:52:04.709761Z", + "iopub.status.idle": "2026-06-07T20:52:04.791436Z", + "shell.execute_reply": "2026-06-07T20:52:04.790452Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unit conversion not available: Property `Width` does not own any\n", + " unit with respect to the semantics applied.\n" + ] + } + ], "source": [ "try:\n", " item.custom_properties.Width.convert_to(\"m\")\n", @@ -208,9 +568,25 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.793830Z", + "iopub.status.busy": "2026-06-07T20:52:04.793678Z", + "iopub.status.idle": "2026-06-07T20:52:04.873432Z", + "shell.execute_reply": "2026-06-07T20:52:04.872431Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unit conversion not available: Property `Length` does not own any\n", + " unit with respect to the semantics applied.\n" + ] + } + ], "source": [ "try:\n", " item.custom_properties.Length.convert_to(\"m\")\n", @@ -227,9 +603,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.875471Z", + "iopub.status.busy": "2026-06-07T20:52:04.875312Z", + "iopub.status.idle": "2026-06-07T20:52:04.879181Z", + "shell.execute_reply": "2026-06-07T20:52:04.878411Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Width': 0.5, 'Length': 0.15}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.custom_properties.model_dump(flat=True)" ] @@ -237,11 +631,33 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 2.x. Setting access properties\n\nAccess control is defined via `access_properties`, which assigns roles to specific users and groups. The available roles are `MEMBER` (read only), `CONTRIBUTOR` (read and update), and `OWNER` (read, update, delete, manage)." + "source": [ + "### 2.x. Setting access properties\n", + "\n", + "Access control is defined via `access_properties`, which assigns roles to specific users and groups. The available roles are `MEMBER` (read only), `CONTRIBUTOR` (read and update), and `OWNER` (read, update, delete, manage)." + ] }, { "cell_type": "code", - "metadata": {}, + "execution_count": 14, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:04.880881Z", + "iopub.status.busy": "2026-06-07T20:52:04.880744Z", + "iopub.status.idle": "2026-06-07T20:52:05.038642Z", + "shell.execute_reply": "2026-06-07T20:52:05.037445Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/core/dsms.py:222: UserWarning: Nothing to commit. No changes have been made to the DSMS instance.If you would like to add&/delete KItems, KTypes or AppConfigs,please use: `dsms.add(my_object)` or dsms.delete(my_object)`before running `dsms.commit()`.\n", + " warnings.warn(\n" + ] + } + ], "source": [ "from dsms.knowledge.properties.access import KItemAccessProperties, Role\n", "\n", @@ -255,9 +671,7 @@ " user_access=[{\"user_id\": current_user.id, \"role\": Role.OWNER}],\n", ")\n", "dsms.commit()" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -288,7 +702,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/3_updating.ipynb b/docs/dsms_sdk/tutorials/3_updating.ipynb index a0464e2..2490cde 100644 --- a/docs/dsms_sdk/tutorials/3_updating.ipynb +++ b/docs/dsms_sdk/tutorials/3_updating.ipynb @@ -23,8 +23,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:08.957468Z", + "iopub.status.busy": "2026-06-07T20:52:08.957268Z", + "iopub.status.idle": "2026-06-07T20:52:09.723609Z", + "shell.execute_reply": "2026-06-07T20:52:09.722427Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem" @@ -39,8 +46,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:09.726448Z", + "iopub.status.busy": "2026-06-07T20:52:09.726044Z", + "iopub.status.idle": "2026-06-07T20:52:10.252776Z", + "shell.execute_reply": "2026-06-07T20:52:10.251529Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -56,9 +70,80 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:10.256562Z", + "iopub.status.busy": "2026-06-07T20:52:10.256369Z", + "iopub.status.idle": "2026-06-07T20:52:12.724457Z", + "shell.execute_reply": "2026-06-07T20:52:12.723321Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:430: UserWarning: A flat dictionary was provided for custom properties.\n", + " Will be transformed into `KItemCustomPropertiesModel`.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 1d2ba175-d61f-4e31-85f6-0bc6830b5cb8\n", + " name: Specimen123\n", + " ktype_id: specimen\n", + " slug: specimen123-1d2ba175\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:10.917226\n", + " updated_at: 2026-06-07 20:52:10.917226\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item = KItem(\n", " name=\"Specimen123\",\n", @@ -72,9 +157,71 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:12.726584Z", + "iopub.status.busy": "2026-06-07T20:52:12.726423Z", + "iopub.status.idle": "2026-06-07T20:52:12.731697Z", + "shell.execute_reply": "2026-06-07T20:52:12.730855Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 1d2ba175-d61f-4e31-85f6-0bc6830b5cb8\n", + " name: Specimen123\n", + " ktype_id: specimen\n", + " slug: specimen123-1d2ba175\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:10.917226\n", + " updated_at: 2026-06-07 20:52:10.917226\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item" ] @@ -101,8 +248,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:12.733434Z", + "iopub.status.busy": "2026-06-07T20:52:12.733251Z", + "iopub.status.idle": "2026-06-07T20:52:12.736005Z", + "shell.execute_reply": "2026-06-07T20:52:12.735628Z" + } + }, "outputs": [], "source": [ "item.name = \"Specimen-123\"\n", @@ -117,8 +271,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:12.738354Z", + "iopub.status.busy": "2026-06-07T20:52:12.738192Z", + "iopub.status.idle": "2026-06-07T20:52:15.210359Z", + "shell.execute_reply": "2026-06-07T20:52:15.209237Z" + } + }, "outputs": [], "source": [ "dsms.add(item)\n", @@ -141,9 +302,79 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:15.213492Z", + "iopub.status.busy": "2026-06-07T20:52:15.213316Z", + "iopub.status.idle": "2026-06-07T20:52:15.220028Z", + "shell.execute_reply": "2026-06-07T20:52:15.219136Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 1d2ba175-d61f-4e31-85f6-0bc6830b5cb8\n", + " name: Specimen-123\n", + " ktype_id: specimen\n", + " slug: specimen123-1d2ba175\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/pmd/co/Specimen\n", + " label: Specimen\n", + " namespace: https://w3id.org/pmd/co\n", + " attachments:\n", + " - name: testfile.txt\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts:\n", + " - name: Specimen preparation\n", + " email: specimenpreparation@group.mail\n", + " created_at: 2026-06-07 20:52:10.917226\n", + " updated_at: 2026-06-07 20:52:13.420118\n", + " external_links:\n", + " - label: specimen-link\n", + " url: http://specimens.org\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 1\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item" ] @@ -159,7 +390,25 @@ }, { "cell_type": "code", - "metadata": {}, + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:15.222007Z", + "iopub.status.busy": "2026-06-07T20:52:15.221855Z", + "iopub.status.idle": "2026-06-07T20:52:15.392370Z", + "shell.execute_reply": "2026-06-07T20:52:15.390871Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/core/dsms.py:222: UserWarning: Nothing to commit. No changes have been made to the DSMS instance.If you would like to add&/delete KItems, KTypes or AppConfigs,please use: `dsms.add(my_object)` or dsms.delete(my_object)`before running `dsms.commit()`.\n", + " warnings.warn(\n" + ] + } + ], "source": [ "from dsms.knowledge.properties.access import KItemAccessProperties, Role\n", "\n", @@ -173,9 +422,7 @@ " user_access=[{\"user_id\": current_user.id, \"role\": Role.CONTRIBUTOR}],\n", ")\n", "dsms.commit()" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -186,9 +433,30 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:15.394803Z", + "iopub.status.busy": "2026-06-07T20:52:15.394632Z", + "iopub.status.idle": "2026-06-07T20:52:15.622891Z", + "shell.execute_reply": "2026-06-07T20:52:15.621435Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\t\t\t Downloaded file: testfile.txt\n", + "|------------------------------------Beginning of file------------------------------------|\n", + "This is a specimen preparation protocol!\n", + "\n", + "|---------------------------------------End of file---------------------------------------|\n", + "\n", + "\n" + ] + } + ], "source": [ "for file in item.attachments:\n", " download = file.download()\n", @@ -201,8 +469,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:15.625361Z", + "iopub.status.busy": "2026-06-07T20:52:15.625160Z", + "iopub.status.idle": "2026-06-07T20:52:16.116613Z", + "shell.execute_reply": "2026-06-07T20:52:16.115309Z" + } + }, "outputs": [], "source": [ "# Clean up the tutorial item\n", @@ -227,7 +502,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/4_deletion.ipynb b/docs/dsms_sdk/tutorials/4_deletion.ipynb index ac6a3e1..3331135 100644 --- a/docs/dsms_sdk/tutorials/4_deletion.ipynb +++ b/docs/dsms_sdk/tutorials/4_deletion.ipynb @@ -4,20 +4,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Deleting KItems with the SDK\n\nIn this tutorial we see how to delete KItems and their properties." + "# 4. Deleting KItems with the SDK\n", + "\n", + "In this tutorial we see how to delete KItems and their properties." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 4.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nNow let us import the needed classes and functions for this tutorial." + "### 4.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "Now let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:20.126707Z", + "iopub.status.busy": "2026-06-07T20:52:20.126569Z", + "iopub.status.idle": "2026-06-07T20:52:20.769257Z", + "shell.execute_reply": "2026-06-07T20:52:20.768257Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem\n", @@ -34,8 +47,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:20.772050Z", + "iopub.status.busy": "2026-06-07T20:52:20.771763Z", + "iopub.status.idle": "2026-06-07T20:52:21.285270Z", + "shell.execute_reply": "2026-06-07T20:52:21.284239Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -51,9 +71,89 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:21.287942Z", + "iopub.status.busy": "2026-06-07T20:52:21.287782Z", + "iopub.status.idle": "2026-06-07T20:52:24.304520Z", + "shell.execute_reply": "2026-06-07T20:52:24.303800Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:430: UserWarning: A flat dictionary was provided for custom properties.\n", + " Will be transformed into `KItemCustomPropertiesModel`.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 401e2fad-8a68-4b95-b044-d023f10ae662\n", + " name: SpecimenToDelete\n", + " ktype_id: specimen\n", + " slug: specimentodelete-401e2fad\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/pmd/co/Specimen\n", + " label: Specimen\n", + " namespace: https://w3id.org/pmd/co\n", + " attachments:\n", + " - name: testfile.txt\n", + " linked_kitems: []\n", + " affiliations:\n", + " - name: Example Institute\n", + " authors: []\n", + " contacts:\n", + " - name: Tutorial Contact\n", + " email: contact@example.com\n", + " created_at: 2026-06-07 20:52:21.967502\n", + " updated_at: 2026-06-07 20:52:21.967502\n", + " external_links:\n", + " - label: Example link\n", + " url: https://example.com\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Create a Specimen with properties to demonstrate deletion\n", "item = KItem(\n", @@ -93,9 +193,29 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:24.306977Z", + "iopub.status.busy": "2026-06-07T20:52:24.306818Z", + "iopub.status.idle": "2026-06-07T20:52:24.310641Z", + "shell.execute_reply": "2026-06-07T20:52:24.310036Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "contact:\n", + " name: Tutorial Contact\n", + " email: contact@example.com" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item.attachments.pop(0)\n", "item.annotations.pop(0)\n", @@ -112,8 +232,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:24.312411Z", + "iopub.status.busy": "2026-06-07T20:52:24.312259Z", + "iopub.status.idle": "2026-06-07T20:52:24.314644Z", + "shell.execute_reply": "2026-06-07T20:52:24.314036Z" + } + }, "outputs": [], "source": [ "item.affiliations = []" @@ -128,8 +255,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:24.316340Z", + "iopub.status.busy": "2026-06-07T20:52:24.316191Z", + "iopub.status.idle": "2026-06-07T20:52:26.502185Z", + "shell.execute_reply": "2026-06-07T20:52:26.501032Z" + } + }, "outputs": [], "source": [ "dsms.add(item)\n", @@ -152,9 +286,76 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:26.504688Z", + "iopub.status.busy": "2026-06-07T20:52:26.504528Z", + "iopub.status.idle": "2026-06-07T20:52:26.510827Z", + "shell.execute_reply": "2026-06-07T20:52:26.509943Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 401e2fad-8a68-4b95-b044-d023f10ae662\n", + " name: SpecimenToDelete\n", + " ktype_id: specimen\n", + " slug: specimentodelete-401e2fad\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations:\n", + " - name: Example Institute\n", + " authors: []\n", + " contacts:\n", + " - name: Tutorial Contact\n", + " email: contact@example.com\n", + " created_at: 2026-06-07 20:52:21.967502\n", + " updated_at: 2026-06-07 20:52:21.967502\n", + " external_links:\n", + " - label: Example link\n", + " url: https://example.com\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item" ] @@ -168,8 +369,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:26.512275Z", + "iopub.status.busy": "2026-06-07T20:52:26.512127Z", + "iopub.status.idle": "2026-06-07T20:52:26.514536Z", + "shell.execute_reply": "2026-06-07T20:52:26.513876Z" + } + }, "outputs": [], "source": [ "del dsms[item]" @@ -185,8 +393,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:26.516218Z", + "iopub.status.busy": "2026-06-07T20:52:26.516075Z", + "iopub.status.idle": "2026-06-07T20:52:26.994428Z", + "shell.execute_reply": "2026-06-07T20:52:26.993049Z" + } + }, "outputs": [], "source": [ "dsms.commit()" @@ -194,9 +409,433 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:26.996981Z", + "iopub.status.busy": "2026-06-07T20:52:26.996825Z", + "iopub.status.idle": "2026-06-07T20:52:30.035281Z", + "shell.execute_reply": "2026-06-07T20:52:30.034334Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/core/dsms.py:339: DeprecationWarning: `kitems`-property is deprecated and only returns the 10 first kitems.\n", + " Please use the `get_kitems`-method instead.\n", + " warnings.warn(message, DeprecationWarning)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Project`. Cannot check if value is of correct type.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Tensile Strength [MPa]`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Material`. Cannot check if value is of correct type.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "kitems:\n", + "- id: 032a3d86-4705-47c0-969e-3251db82a1a6\n", + " name: my tensile test experiment\n", + " ktype_id: dataset\n", + " slug: mytensiletestexperiment-032a3d86\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: dummy_data.csv\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-05 15:55:53.758604\n", + " updated_at: 2026-06-05 15:55:53.758604\n", + " external_links: []\n", + " apps:\n", + " - executable: testapp2\n", + " title: data2rdf\n", + " description: null\n", + " tags: null\n", + " additional_properties:\n", + " triggerUponUpload: true\n", + " triggerUponUploadFileExtensions:\n", + " - .csv\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 05884075-236c-483a-8427-1c9cd8886a04\n", + " name: test 2\n", + " ktype_id: metal-sheet\n", + " slug: test-2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: bb98191c-9695-4980-a655-cad03abde223\n", + " name: Metal sheet test\n", + " ktype_id: metal-sheet\n", + " slug: metal-sheet-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 5f2868e2-1196-4fb9-adb7-3b3d68e0a1f3\n", + " name: Specimen Extraction 2618A\n", + " ktype_id: manufacturing-process\n", + " slug: specimen-extraction-2618a\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-04 18:35:46.248292\n", + " updated_at: 2026-06-04 18:35:46.248292\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " - role: CONTRIBUTOR\n", + " user_id: b76fa8d6-20d8-4a7e-a959-a975082b1c63\n", + " group_access: []\n", + " contexts:\n", + " - id: 249e1cdf-f584-4568-86cb-198757c66c15\n", + " name: metal sheet batch test\n", + " ktype_id: metal-sheet-batch\n", + " slug: metal-sheet-batch-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + "- id: 1053cd9b-221a-40cc-adb6-517cdcaa174c\n", + " name: My creep speciment\n", + " ktype_id: creep-specimen\n", + " slug: my-creep-speciment\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/steel/ProcessOntology/PercentageExtension\n", + " label: PercentageExtension\n", + " namespace: steelontology\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-05-28 08:49:52.034919\n", + " updated_at: 2026-05-28 08:49:52.034919\n", + " external_links: []\n", + " apps: []\n", + " summary: ''\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-creep-gauge\n", + " name: Gauge Section\n", + " entries:\n", + " - id: input-creep-gauge-length\n", + " type: Number\n", + " label: Gauge length\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasGaugeLengthLo_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: input-creep-gauge-diameter\n", + " type: Number\n", + " label: Gauge diameter\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasDiameter_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: section-creep-grip\n", + " name: Grip Ends\n", + " entries:\n", + " - id: input-creep-grip-type\n", + " type: Select\n", + " label: Grip type\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleType_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: input-creep-thread-spec\n", + " type: Text\n", + " label: Thread specification\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: https://w3id.org/steel/ProcessOntology/hasSampleInfos_Object\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 164c10c3-c84a-42de-abe0-41423d1091ad\n", + " name: Test catalog\n", + " ktype_id: dataset-catalog\n", + " slug: testcatalog-164c10c3\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-05 14:49:07.538315\n", + " updated_at: 2026-06-05 14:49:07.538315\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: id178067094693834dl8n\n", + " name: Misc\n", + " entries:\n", + " - id: id1780670946938uw41nf\n", + " type: Text\n", + " label: Project\n", + " value: Mechanical testing campaign 1\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts:\n", + " - id: 6042535e-0a9e-4233-a09e-bd4464a695fc\n", + " name: Test dataset\n", + " ktype_id: dataset\n", + " slug: testdataset-6042535e\n", + " avatar_exists: false\n", + " has_contexts: false\n", + "- id: 1800e831-f267-4fa7-a1e6-1acda6dba9b0\n", + " name: Test\n", + " ktype_id: chemical-composition\n", + " slug: test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-05-03 16:39:29.875453\n", + " updated_at: 2026-05-03 16:39:29.875453\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 227d1678-aa56-4598-9082-540c629fd8a8\n", + " name: Specimen123\n", + " ktype_id: specimen\n", + " slug: specimen123-227d1678\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:02.878207\n", + " updated_at: 2026-06-07 20:52:02.878207\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 0.5\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 249e1cdf-f584-4568-86cb-198757c66c15\n", + " name: metal sheet batch test\n", + " ktype_id: metal-sheet-batch\n", + " slug: metal-sheet-batch-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 5f2868e2-1196-4fb9-adb7-3b3d68e0a1f3\n", + " name: Specimen Extraction 2618A\n", + " ktype_id: manufacturing-process\n", + " slug: specimen-extraction-2618a\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-04 14:22:32.661556\n", + " updated_at: 2026-06-04 14:22:32.661556\n", + " external_links: []\n", + " apps: []\n", + " summary: ''\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 2da465db-0840-4a1e-a24b-cc7355498958\n", + " name: Material test\n", + " ktype_id: material\n", + " slug: material-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-05-27 22:31:56.175890\n", + " updated_at: 2026-05-27 22:31:56.175890\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "- id: 33d13760-d0ce-4117-bbef-c03970ae4d09\n", + " name: nb8-test-project\n", + " ktype_id: project\n", + " slug: nb8-test-project-33d13760\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-05 15:09:22.371655\n", + " updated_at: 2026-06-05 15:09:22.371655\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts:\n", + " - id: e2538d23-d9bd-4151-94c4-c691d89613a6\n", + " name: nb8-test-dataset\n", + " ktype_id: dataset\n", + " slug: nb8-test-dataset-e2538d23\n", + " avatar_exists: false\n", + " has_contexts: false\n", + "- id: 3ddb5765-3fda-4ed2-9e7a-5459197ffe6f\n", + " name: Test project\n", + " ktype_id: project\n", + " slug: testproject-3ddb5765\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-05 15:51:44.846169\n", + " updated_at: 2026-06-05 15:51:44.846169\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "total_count: 39" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "dsms.kitems" ] @@ -229,7 +868,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/5_search.ipynb b/docs/dsms_sdk/tutorials/5_search.ipynb index c123e01..26ec61d 100644 --- a/docs/dsms_sdk/tutorials/5_search.ipynb +++ b/docs/dsms_sdk/tutorials/5_search.ipynb @@ -4,20 +4,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 5. Searching KItems with the SDK\n\nIn this tutorial we see how to search existing KItems" + "# 5. Searching KItems with the SDK\n", + "\n", + "In this tutorial we see how to search existing KItems" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 5.1. Setting up\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\n\nNow let us import the needed classes and functions for this tutorial." + "### 5.1. Setting up\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "\n", + "Now let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:33.740072Z", + "iopub.status.busy": "2026-06-07T20:52:33.739898Z", + "iopub.status.idle": "2026-06-07T20:52:34.393563Z", + "shell.execute_reply": "2026-06-07T20:52:34.392399Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem" @@ -32,8 +45,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:34.396930Z", + "iopub.status.busy": "2026-06-07T20:52:34.396592Z", + "iopub.status.idle": "2026-06-07T20:52:34.904662Z", + "shell.execute_reply": "2026-06-07T20:52:34.903487Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -51,16 +71,81 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this section, we would like to search for specific KItems we created in the DSMS.\n\nFor this purpose, we will firstly create some KItems and apply the `search`-method on the `DSMS`-object later on in order to find them again in the DSMS.\n\nWe also want to demonstrate here, that we can link KItems to each other in order to find e.g. a related item of type `DatasetCatalog`. For this strategy, we are using the `linked_kitems`- attribute and the `id` of the item which we would like to link.\n\nThe procedure looks like this:" + "In this section, we would like to search for specific KItems we created in the DSMS.\n", + "\n", + "For this purpose, we will firstly create some KItems and apply the `search`-method on the `DSMS`-object later on in order to find them again in the DSMS.\n", + "\n", + "We also want to demonstrate here, that we can link KItems to each other in order to find e.g. a related item of type `DatasetCatalog`. For this strategy, we are using the `linked_kitems`- attribute and the `id` of the item which we would like to link.\n", + "\n", + "The procedure looks like this:" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:34.908633Z", + "iopub.status.busy": "2026-06-07T20:52:34.908447Z", + "iopub.status.idle": "2026-06-07T20:52:46.334364Z", + "shell.execute_reply": "2026-06-07T20:52:46.333400Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:330: UserWarning: Found a to be linked instead of an . Will link it with the default relationship 'dcterms:haspart'.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:430: UserWarning: A flat dictionary was provided for custom properties.\n", + " Will be transformed into `KItemCustomPropertiesModel`.\n", + " warnings.warn(\n" + ] + } + ], "source": [ - "item1 = KItem(\n name=\"Machine-1\",\n ktype_id=dsms.ktypes.MeasurementDevice\n)\n\nitem2 = KItem(\n name=\"Machine-2\",\n ktype_id=dsms.ktypes.MeasurementDevice\n)\n\nitem3 = KItem(\n name=\"Specimen-1\", \n ktype_id=dsms.ktypes.Specimen,\n linked_kitems=[item1],\n annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n custom_properties = {\n \"Width\": 0.5,\n \"Length\": 0.15,\n }\n\n)\nitem4 = KItem(\n name=\"Specimen-2\",\n ktype_id=dsms.ktypes.Specimen,\n linked_kitems=[item2],\n annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n custom_properties = {\n \"Width\": 0.8,\n \"Length\": 0.85,\n }\n)\n\nitem5 = KItem(\n name=\"Research Institute ABC\",\n ktype_id=dsms.ktypes.Organization,\n linked_kitems=[item1,item2],\n annotations=[\"www.researchBACiri.org/foo\"],\n)\n\ndsms.add([item1, item2, item3, item4, item5])\ndsms.commit()" + "item1 = KItem(\n", + " name=\"Machine-1\",\n", + " ktype_id=dsms.ktypes.MeasurementDevice\n", + ")\n", + "\n", + "item2 = KItem(\n", + " name=\"Machine-2\",\n", + " ktype_id=dsms.ktypes.MeasurementDevice\n", + ")\n", + "\n", + "item3 = KItem(\n", + " name=\"Specimen-1\", \n", + " ktype_id=dsms.ktypes.Specimen,\n", + " linked_kitems=[item1],\n", + " annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n", + " custom_properties = {\n", + " \"Width\": 0.5,\n", + " \"Length\": 0.15,\n", + " }\n", + "\n", + ")\n", + "item4 = KItem(\n", + " name=\"Specimen-2\",\n", + " ktype_id=dsms.ktypes.Specimen,\n", + " linked_kitems=[item2],\n", + " annotations=[\"https://w3id.org/steel/ProcessOntology/TestPiece\"],\n", + " custom_properties = {\n", + " \"Width\": 0.8,\n", + " \"Length\": 0.85,\n", + " }\n", + ")\n", + "\n", + "item5 = KItem(\n", + " name=\"Research Institute ABC\",\n", + " ktype_id=dsms.ktypes.Organization,\n", + " linked_kitems=[item1,item2],\n", + " annotations=[\"www.researchBACiri.org/foo\"],\n", + ")\n", + "\n", + "dsms.add([item1, item2, item3, item4, item5])\n", + "dsms.commit()" ] }, { @@ -74,14 +159,154 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\n

Note : Here in this tutorial, we use dsms.search with `limit=2` to maintain readability but the user can adjust the variable `limit` as per requirement.

\n\n\nNow, we can search for e.g. KItems of type `MeasurementDevice`:" + "\n", + "

Note : Here in this tutorial, we use dsms.search with `limit=2` to maintain readability but the user can adjust the variable `limit` as per requirement.

\n", + "\n", + "\n", + "Now, we can search for e.g. KItems of type `MeasurementDevice`:" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:46.337676Z", + "iopub.status.busy": "2026-06-07T20:52:46.337509Z", + "iopub.status.idle": "2026-06-07T20:52:47.033167Z", + "shell.execute_reply": "2026-06-07T20:52:47.032218Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Material State`. Cannot check if value is of correct type.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hits:\n", + "- kitem:\n", + " id: f2320732-5825-404a-a5bc-2283267569ef\n", + " name: Over-aged HP-180-1 after ageing\n", + " ktype_id: specimen\n", + " slug: over-agedhp-180-1afterageing-f2320732\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/ORCHESTER/heat-treatment/AgedSpecimen\n", + " label: AgedSpecimen\n", + " namespace: https://w3id.org/ORCHESTER/heat-treatment\n", + " attachments: []\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: fe7692ed-c3aa-44dd-9a65-4ec1400ba358\n", + " name: Heat treatment for HP-180-1\n", + " ktype_id: manufacturing-process\n", + " slug: heattreatmentforhp-180-1-fe7692ed\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2025-02-14 08:44:54.658452\n", + " updated_at: 2025-02-14 08:44:54.658452\n", + " external_links: []\n", + " apps: []\n", + " summary: \"Specimen HP-180-1 after ageing with 180 \\xB0C, 1.1 h\"\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: id17395226943636hrlge\n", + " name: Misc\n", + " entries:\n", + " - id: id17395226943631a7aru\n", + " type: Text\n", + " label: Material State\n", + " value: \"180 \\xB0C, 1.1 h\"\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: b556dd81-c80c-4661-bca9-6d7b8614b1fe\n", + " group_access: []\n", + " contexts: []\n", + " fuzzy: false\n", + "- kitem:\n", + " id: c42a54a0-0d6d-4691-b3d7-b7fc51902886\n", + " name: ABR4RTd2 before ageing\n", + " ktype_id: specimen\n", + " slug: abr4rtd2beforeageing-c42a54a0\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: d3b4f599-a37b-47cc-ac0c-78ccfff3b604\n", + " name: Heat treatment for ABR4RTd2\n", + " ktype_id: manufacturing-process\n", + " slug: heattreatmentforabr4rtd2-d3b4f599\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 5f2868e2-1196-4fb9-adb7-3b3d68e0a1f3\n", + " name: Specimen Extraction 2618A\n", + " ktype_id: manufacturing-process\n", + " slug: specimen-extraction-2618a\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2025-02-14 08:43:27.618862\n", + " updated_at: 2025-02-14 08:43:27.618862\n", + " external_links: []\n", + " apps: []\n", + " summary: \"ABR4RTd2 at T61 state before ageing with 160 \\xB0C, 8760 h\"\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: id1739522607214d9ypef\n", + " name: Misc\n", + " entries:\n", + " - id: id173952260721459lyeg\n", + " type: Text\n", + " label: Material State\n", + " value: T61\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: b556dd81-c80c-4661-bca9-6d7b8614b1fe\n", + " group_access: []\n", + " contexts: []\n", + " fuzzy: false\n", + "total_count: 184\n", + "\n", + "Name of the first kitem:\n", + "Over-aged HP-180-1 after ageing\n" + ] + } + ], "source": [ "result = dsms.search(ktypes=[dsms.ktypes.Specimen], limit=2)\n", "print(result)\n", @@ -98,11 +323,489 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:47.035596Z", + "iopub.status.busy": "2026-06-07T20:52:47.035438Z", + "iopub.status.idle": "2026-06-07T20:52:47.834132Z", + "shell.execute_reply": "2026-06-07T20:52:47.832998Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kitem:\n", + " id: 0509a638-227b-4a57-a381-5a591748facb\n", + " name: Fraunhofer IWM\n", + " ktype_id: organization\n", + " slug: fraunhofer-iwm\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/steel/ProcessOntology/TestingFacility\n", + " label: TestingFacility\n", + " namespace: steel-ontology\n", + " - iri: https://w3id.org/steel/ProcessOntology/MaterialsScience\n", + " label: MaterialsScience\n", + " namespace: steel-ontology\n", + " - iri: https://w3id.org/steel/ProcessOntology/Manufacturing\n", + " label: Manufacturing\n", + " namespace: steel-ontology\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems:\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: 3c4585fb-eec5-4808-8e73-fc853c8a4a0e\n", + " name: \"Johannes Preu\\xDFner\"\n", + " ktype_id: expert\n", + " slug: johannes-preussner\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: 00e8b5a3-c65d-4d1a-85d4-5607e8048224\n", + " name: Yoav Nahshon\n", + " ktype_id: expert\n", + " slug: yoav-nahshon\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id220e33fbd6ce1\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: b10cb7e7-c48e-4405-b7ba-7788ee425db4\n", + " name: ORCHESTER\n", + " ktype_id: project\n", + " slug: orchester\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: idb13d750ebf668\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: 7dc50ec2-ce28-4c8b-be94-de533f7b71c7\n", + " name: \"Gesch\\xE4ftsfeld Werkstoffbewertung und Lebensdauerkonzepte\"\n", + " ktype_id: organization\n", + " slug: geschftsfeld-werkstoffbewertung-und-lebe\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id2af8ad6c4bb5a\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: bb98191c-9695-4980-a655-cad03abde223\n", + " name: Metal sheet test\n", + " ktype_id: metal-sheet\n", + " slug: metal-sheet-test\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: f7114f25-a437-430d-b4a3-cb58b6d28b34\n", + " name: Dirk Helm\n", + " ktype_id: expert\n", + " slug: dirk-helm\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id220e33fbd6ce1\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: f382ef00-bb80-4373-96c8-59b302b96d41\n", + " name: Alu-Vorprojekte\n", + " ktype_id: project\n", + " slug: alu-vorprojekte\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: idb13d750ebf668\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: fc8b2c3e-358d-46a9-8f5d-811a43f90875\n", + " name: \"Matthias B\\xFCschelberger\"\n", + " ktype_id: expert\n", + " slug: matthias-bschelberger\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id220e33fbd6ce1\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: 8ea65573-67fe-4e7b-85ce-e2edd512e192\n", + " name: High Cycle Fatigue EN AW-2618A\n", + " ktype_id: dataset-catalog\n", + " slug: high-cycle-fatigue-en-aw-2618a\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: b0c08a47-ad86-459d-83a7-c33310731790\n", + " name: \"Gesch\\xE4ftsfeld Fertigungsprozesse\"\n", + " ktype_id: organization\n", + " slug: geschftsfeld-fertigungsprozesse\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id2af8ad6c4bb5a\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: 97611992-97cd-40e2-bec8-2c0becab07d8\n", + " name: Christoph Schweizer\n", + " ktype_id: expert\n", + " slug: christoph-schweizer\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id220e33fbd6ce1\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 455d854f-889e-48e6-8924-7fc78e77fe00\n", + " name: Final Report FVV No. 1390\n", + " ktype_id: document\n", + " slug: final-report-fvv-no-1390\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 053142ba-d882-441a-b8c5-ea8f889e8527\n", + " name: Damage Parameter DFC for creep-fatigue-interaction\n", + " ktype_id: material-model\n", + " slug: damage-parameter-dfc-for-creep-fatigue-i\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: c83db95e-bef8-4bd5-a2fc-ab645437309d\n", + " name: Universal sheet metal testing machine\n", + " ktype_id: measurement-device\n", + " slug: universal-sheet-metal-testing-machine-bu\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id7550e1c6439dd\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: eacb6b34-48ce-4ae0-83f0-b5f0a2d376fa\n", + " name: StahlDigital\n", + " ktype_id: project\n", + " slug: stahldigital\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: idb13d750ebf668\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: abbe76e4-e09c-4448-ad8e-56ec8d5843f0\n", + " name: Fraunhofer\n", + " ktype_id: organization\n", + " slug: fraunhofer\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id2af8ad6c4bb5a\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: e6da3725-ecfd-4658-82ac-9058a118fb26\n", + " name: Fraunhofer IWM\n", + " ktype_id: address\n", + " slug: fraunhofer-iwm\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id5597c3fd52603\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2024-07-28 21:17:03.942225\n", + " updated_at: 2024-07-28 21:17:03.942225\n", + " external_links:\n", + " - label: _u_r_l\n", + " url: https://www.iwm.fraunhofer.de/\n", + " apps: []\n", + " summary: \"Das Fraunhofer-Institut f\\xFCr Werkstoffmechanik IWM in Freiburg ist eine\\\n", + " \\ Einrichtung der Fraunhofer-Gesellschaft zur F\\xF6rderung der angewandten Forschung\\\n", + " \\ e.V. Das Fraunhofer IWM ist Forschungs- und Entwicklungspartner der Industrie\\\n", + " \\ und von \\xF6ffentlichen Auftraggebern zu Themen der Sicherheit, Zuverl\\xE4ssigkeit,\\\n", + " \\ Lebensdauer und Funktionalit\\xE4t von Werkstoffen in Bauteilen und Fertigungsverfahren.\"\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: id21866f42d5176\n", + " name: Untitled Section\n", + " entries:\n", + " - id: id4d61120f8538b\n", + " type: Text\n", + " label: Name of the Organisation\n", + " value: Fraunhofer IWM\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id2af8ad6c4bb5a\n", + " type: Knowledge item\n", + " label: Sub Organisation of\n", + " value:\n", + " - id: abbe76e4-e09c-4448-ad8e-56ec8d5843f0\n", + " name: Fraunhofer\n", + " ktype_id: organization\n", + " slug: fraunhofer\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#subOrganizationOf\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id5880a22a80ecf\n", + " type: Text\n", + " label: URL of the webpage\n", + " value: https://www.iwm.fraunhofer.de\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#accessURL\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id3e32c796f00c7\n", + " type: Text\n", + " label: City\n", + " value: Freiburg\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id5597c3fd52603\n", + " type: Knowledge item\n", + " label: Address\n", + " value:\n", + " - id: e6da3725-ecfd-4658-82ac-9058a118fb26\n", + " name: Fraunhofer IWM\n", + " ktype_id: address\n", + " slug: fraunhofer-iwm\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 1f6980ba-f01e-4d32-83d5-82397db4c5e9\n", + " group_access: []\n", + " contexts: []\n", + "\n", + "fuzziness: False\n", + "\n", + "\n", + "kitem:\n", + " id: 0eb837b5-dd1d-48dc-8ee7-89d4c373d0a2\n", + " name: Gruppe Werkstoffcharakterisierung (IWS)\n", + " ktype_id: organization\n", + " slug: gruppe-werkstoffcharakterisierung-iws\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments:\n", + " - name: KG.kitem.ttl\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 81fa7693-ca78-443a-99e4-f8233e5e765b\n", + " name: ORC_S1_V2_01_316L_14057_ab_QS\n", + " ktype_id: specimen\n", + " slug: orcs1v201316l14057abqs\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: ee7206b8-aeb2-4b6b-9679-159e548b215a\n", + " name: Leonid Gerdt\n", + " ktype_id: expert\n", + " slug: leonid-gerdt\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 04f1d7d3-4bd4-43d1-ae33-d8f50814dcc3\n", + " name: ORC_S1_V1_08_316L_PANACEA_WB_LS\n", + " ktype_id: specimen\n", + " slug: orcs1v108316lpanaceawbls\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 173c87c7-298e-451c-ad63-2933f5f2d907\n", + " name: Fraunhofer IWS\n", + " ktype_id: organization\n", + " slug: fraunhofer-iws\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id2af8ad6c4bb5a\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: d7d328d4-174d-4b64-ba81-574ed808e68d\n", + " name: \"Philip Tr\\xE4ger\"\n", + " ktype_id: expert\n", + " slug: philip-traeger\n", + " avatar_exists: true\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: true\n", + " label: Has Part\n", + " kitem:\n", + " id: fb7ca4e0-c80c-4b7c-b6f0-e8515deac2f5\n", + " name: Sebastian Biastoch\n", + " ktype_id: expert\n", + " slug: sebastian-biastoch\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " generated_by: id220e33fbd6ce1\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2024-08-01 09:09:33.415077\n", + " updated_at: 2024-08-01 09:09:33.415077\n", + " external_links: []\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: id21866f42d5176\n", + " name: Untitled Section\n", + " entries:\n", + " - id: id4d61120f8538b\n", + " type: Text\n", + " label: Name of the Organisation\n", + " value: Gruppe Werkstoffcharakterisierung (IWS)\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://xmlns.com/foaf/0.1/name\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id2af8ad6c4bb5a\n", + " type: Knowledge item\n", + " label: Sub Organisation of\n", + " value:\n", + " - id: 173c87c7-298e-451c-ad63-2933f5f2d907\n", + " name: Fraunhofer IWS\n", + " ktype_id: organization\n", + " slug: fraunhofer-iws\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#subOrganizationOf\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id5880a22a80ecf\n", + " type: Text\n", + " label: URL of the webpage\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#accessURL\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id3e32c796f00c7\n", + " type: Text\n", + " label: City\n", + " value: Dresden\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " - id: id5597c3fd52603\n", + " type: Knowledge item\n", + " label: Address\n", + " value: null\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/org#location\n", + " label: null\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " required: false\n", + " access_properties:\n", + " visibility: internal\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 7e842ac6-3642-49bb-971d-e991d991a8e0\n", + " group_access: []\n", + " contexts: []\n", + "\n", + "fuzziness: False\n", + "\n", + "\n" + ] + } + ], "source": [ - "for result in dsms.search(ktypes=[dsms.ktypes.Organization, dsms.ktypes.MeasurementDevice], limit=2):\n print(result.kitem)\n print(\"fuzziness: \", result.fuzzy)\n print(\"\\n\")\n " + "for result in dsms.search(ktypes=[dsms.ktypes.Organization, dsms.ktypes.MeasurementDevice], limit=2):\n", + " print(result.kitem)\n", + " print(\"fuzziness: \", result.fuzzy)\n", + " print(\"\\n\")\n", + " " ] }, { @@ -114,9 +817,76 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:47.836354Z", + "iopub.status.busy": "2026-06-07T20:52:47.836183Z", + "iopub.status.idle": "2026-06-07T20:52:48.252301Z", + "shell.execute_reply": "2026-06-07T20:52:48.251325Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kitem:\n", + " id: 83bbae03-6067-4b07-9713-33c53198f372\n", + " name: Specimen-123\n", + " ktype_id: specimen\n", + " slug: specimen123-83bbae03\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: https://w3id.org/pmd/co/Specimen\n", + " label: Specimen\n", + " namespace: https://w3id.org/pmd/co\n", + " attachments:\n", + " - name: testfile.txt\n", + " linked_kitems: []\n", + " affiliations: []\n", + " contacts:\n", + " - name: Specimen preparation\n", + " email: specimenpreparation@group.mail\n", + " created_at: 2026-06-05 15:45:36.007534\n", + " updated_at: 2026-06-05 15:45:38.058192\n", + " external_links:\n", + " - label: specimen-link\n", + " url: http://specimens.org\n", + " apps: []\n", + " custom_properties:\n", + " content:\n", + " sections:\n", + " - id: section-specimen-info\n", + " name: Specimen Information\n", + " entries:\n", + " - id: input-specimen-width\n", + " type: Number\n", + " label: Width\n", + " value: 1\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " - id: input-specimen-length\n", + " type: Number\n", + " label: Length\n", + " value: 0.15\n", + " measurementUnit: null\n", + " relationMapping: null\n", + " required: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "fuzzy: false\n", + "\n" + ] + } + ], "source": [ "for result in dsms.search(query=\"Specimen-1\", ktypes=[dsms.ktypes.Specimen], allow_fuzzy=False, limit=1):\n", " print(result)" @@ -131,9 +901,71 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.254274Z", + "iopub.status.busy": "2026-06-07T20:52:48.254119Z", + "iopub.status.idle": "2026-06-07T20:52:48.675410Z", + "shell.execute_reply": "2026-06-07T20:52:48.674414Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kitem:\n", + " id: 15f7b8a8-a0ab-450e-80c4-fafc0e454f8c\n", + " name: Research Institute ABC\n", + " ktype_id: organization\n", + " slug: researchinstituteabc-15f7b8a8\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations:\n", + " - iri: www.researchBACiri.org/foo\n", + " label: foo\n", + " namespace: www.researchBACiri.org\n", + " attachments: []\n", + " linked_kitems:\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " - is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 61a91ea0-53c4-4711-aa59-c1d33f75a640\n", + " name: Machine-1\n", + " ktype_id: measurement-device\n", + " slug: machine-1-61a91ea0\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + " affiliations: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:44.716348\n", + " updated_at: 2026-06-07 20:52:44.716348\n", + " external_links: []\n", + " apps: []\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []\n", + "fuzzy: false\n", + "\n" + ] + } + ], "source": [ "for result in dsms.search(\n", " ktypes=[dsms.ktypes.Organization], annotations=[\"www.researchBACiri.org/foo\"], allow_fuzzy=False, limit=1\n", @@ -157,9 +989,46 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.677660Z", + "iopub.status.busy": "2026-06-07T20:52:48.677493Z", + "iopub.status.idle": "2026-06-07T20:52:48.684164Z", + "shell.execute_reply": "2026-06-07T20:52:48.683492Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "- is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart\n", + "- is_incoming: false\n", + " label: Has Part\n", + " kitem:\n", + " id: 61a91ea0-53c4-4711-aa59-c1d33f75a640\n", + " name: Machine-1\n", + " ktype_id: measurement-device\n", + " slug: machine-1-61a91ea0\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " iri: http://purl.org/dc/terms/hasPart" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems" ] @@ -173,9 +1042,51 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.685827Z", + "iopub.status.busy": "2026-06-07T20:52:48.685687Z", + "iopub.status.idle": "2026-06-07T20:52:48.690015Z", + "shell.execute_reply": "2026-06-07T20:52:48.689396Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:38.475617\n", + " updated_at: 2026-06-07 20:52:38.475617\n", + " external_links: []\n", + " apps: []\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems[0].fetch()" ] @@ -189,18 +1100,91 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.691717Z", + "iopub.status.busy": "2026-06-07T20:52:48.691578Z", + "iopub.status.idle": "2026-06-07T20:52:48.695187Z", + "shell.execute_reply": "2026-06-07T20:52:48.694707Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'http://purl.org/dc/terms/hasPart': [kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false,\n", + " kitem:\n", + " id: 61a91ea0-53c4-4711-aa59-c1d33f75a640\n", + " name: Machine-1\n", + " ktype_id: measurement-device\n", + " slug: machine-1-61a91ea0\n", + " avatar_exists: false\n", + " has_contexts: false]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems.by_relation" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.696898Z", + "iopub.status.busy": "2026-06-07T20:52:48.696759Z", + "iopub.status.idle": "2026-06-07T20:52:48.701595Z", + "shell.execute_reply": "2026-06-07T20:52:48.700805Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:38.475617\n", + " updated_at: 2026-06-07 20:52:38.475617\n", + " external_links: []\n", + " apps: []\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems.by_relation[\"http://purl.org/dc/terms/hasPart\"][0].fetch()\n" ] @@ -214,9 +1198,167 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.703110Z", + "iopub.status.busy": "2026-06-07T20:52:48.702972Z", + "iopub.status.idle": "2026-06-07T20:52:48.712002Z", + "shell.execute_reply": "2026-06-07T20:52:48.711212Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{ktype:\n", + " id: measurement-device\n", + " name: Measurement Device\n", + " webform_schema_id: ab1ab1d5-10c6-4096-a99b-3e6fb43478ea\n", + " webform_schema:\n", + " id: ab1ab1d5-10c6-4096-a99b-3e6fb43478ea\n", + " name: Measurement device\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/ORCHESTER/MeasurementDevice\n", + " sections:\n", + " - id: id23f26e1aec6e\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id4771ba047219e\n", + " label: Name of measurement device\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the name of the measurement device\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasMeasurementDeviceName\n", + " type: data_property\n", + " class_iri: https://w3id.org/ORCHESTER/measurement-device/MeasurementDeviceName\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3d095d631a156\n", + " label: Manufacturer\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the name of the manufacturer\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasManufacturer\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id4584da4ce4bc9\n", + " label: Measurement device type\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the measurement type of the device, e.g. microscope\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/MeasurementDeviceType\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id071732bc59fed\n", + " label: Serial number\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the serial number\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasSerialNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id7550e1c6439dd\n", + " label: Responsible organization for the device\n", + " widget: Knowledge item\n", + " required: false\n", + " hint: Please add the responsible department of the organisation\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasResponsibleOrganizationUnit\n", + " type: object_property\n", + " class_iri: https://w3id.org/emmo#EMMO_c0f72631_d7c2_434c_9c26_5c44123df682\n", + " inverse: false\n", + " multiple_selection: false\n", + " knowledge_type: organization\n", + " - id: id2095e14118a78\n", + " label: Room number\n", + " widget: Text\n", + " required: false\n", + " hint: Please add the room number of the device\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasRoomNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: idcce5f8dfb7778\n", + " label: Inventory number\n", + " widget: Text\n", + " required: false\n", + " hint: please add the sigma inventory number\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasInventoryNumber\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3998befe78c2c\n", + " label: Measurement range\n", + " widget: Textarea\n", + " required: false\n", + " hint: Please add relevant information about the measurement range\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: https://w3id.org/ORCHESTER/measurement-device/HasMeasurementRange\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2025-07-29T15:17:44.919187'\n", + " updated_at: '2025-07-29T15:17:44.919187'\n", + " created_at: '2025-02-17T11:22:02.248650'\n", + " updated_at: '2026-05-25T15:17:12.862130': [kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false,\n", + " kitem:\n", + " id: 61a91ea0-53c4-4711-aa59-c1d33f75a640\n", + " name: Machine-1\n", + " ktype_id: measurement-device\n", + " slug: machine-1-61a91ea0\n", + " avatar_exists: false\n", + " has_contexts: false]}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems.by_ktype" ] @@ -230,9 +1372,51 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.713552Z", + "iopub.status.busy": "2026-06-07T20:52:48.713413Z", + "iopub.status.idle": "2026-06-07T20:52:48.717974Z", + "shell.execute_reply": "2026-06-07T20:52:48.717115Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: 50624dc2-ea51-410f-9677-124479ee2684\n", + " name: Machine-2\n", + " ktype_id: measurement-device\n", + " slug: machine-2-50624dc2\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:52:38.475617\n", + " updated_at: 2026-06-07 20:52:38.475617\n", + " external_links: []\n", + " apps: []\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "item5.linked_kitems.by_ktype[dsms.ktypes.MeasurementDevice][0].fetch()" ] @@ -241,14 +1425,142 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 5.4. Filtering by context and attachment type\n\n`DSMS.search()` also supports filtering by context membership and attachment file extensions." + "### 5.4. Filtering by context and attachment type\n", + "\n", + "`DSMS.search()` also supports filtering by context membership and attachment file extensions." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 14, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:48.719636Z", + "iopub.status.busy": "2026-06-07T20:52:48.719382Z", + "iopub.status.idle": "2026-06-07T20:52:55.374297Z", + "shell.execute_reply": "2026-06-07T20:52:55.373218Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ArtificialAgingDurationAsSupposed`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ArtificialAgingTemperatureAsSupposed`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `BaseElementOfComposition`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `CustomerMaterialName`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `FileName`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `HardnessBrinell for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `InternalHeatNumber`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `MaterialID`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `MaximumTensileStrength for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `MaximumTensileStrength for 20 °C (Actual Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Name`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesConductivityTest`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesCrackDetection`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesDimensionalCheck`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesIdentificationCheck`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesTensileTest`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `NumberOfNonConformitiesUltrasonicTesting`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `PercentageElongationAfterFracture for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `PercentageElongationAfterFracture for 20 °C (Actual Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `PercentageIACSElectricalConductivity for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ProcedureName`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ProcessStartDateTime`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ReductionOfAreaAtFracture for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ReductionOfAreaAtFracture for 20 °C (Actual Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Rp02 for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Rp02 for 20 °C (Actual Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `SolutionAnnealingDurationAsSupposed`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `SolutionAnnealingTemperatureAsSupposed`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `VendorHeatNumber`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Si`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Fe`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Cu`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Mn`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Mg`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Cr`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Ni`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Zn`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `WeightFraction for Ti`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `YieldToTensileStrengthRatio for 20 °C (Target Value)`. Cannot check if value is of correct type.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `TimeStamp`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ExtensometerGaugeLength`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `TestingMachine`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `Material`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ProjectName`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `ProjectNumber`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `OriginalCrosssection`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `TestPieceGeometry`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `SampleIdentifier`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `TestTemperature`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `TestStandard`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `OriginalThickness`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `OriginalWidth`. Cannot check if value is of correct type.\n", + " warnings.warn(\n", + "/root/dsms/dsms-python-sdk/dsms/knowledge/kitem.py:714: UserWarning: No webform was defined for entry `CrossheadSeparationRate`. Cannot check if value is of correct type.\n", + " warnings.warn(\n" + ] + } + ], "source": [ "# Find KItems that belong to a specific context (pass the context KItem's ID as a string)\n", "results = dsms.search(contexts=[\"\"])\n", @@ -272,8 +1584,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 15, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:52:55.376739Z", + "iopub.status.busy": "2026-06-07T20:52:55.376585Z", + "iopub.status.idle": "2026-06-07T20:52:57.705339Z", + "shell.execute_reply": "2026-06-07T20:52:57.704829Z" + } + }, "outputs": [], "source": [ "del dsms[item1]\n", @@ -302,7 +1621,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/6_apps.ipynb b/docs/dsms_sdk/tutorials/6_apps.ipynb index 7d515a9..bf2d657 100644 --- a/docs/dsms_sdk/tutorials/6_apps.ipynb +++ b/docs/dsms_sdk/tutorials/6_apps.ipynb @@ -13,13 +13,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 6.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\n\nNow let us import the needed classes and functions for this tutorial." + "### 6.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "\n", + "Now let us import the needed classes and functions for this tutorial." ] }, { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:01.776877Z", + "iopub.status.busy": "2026-06-07T20:53:01.776718Z", + "iopub.status.idle": "2026-06-07T20:53:02.423603Z", + "shell.execute_reply": "2026-06-07T20:53:02.422416Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem, AppConfig\n", @@ -36,7 +48,14 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:02.426289Z", + "iopub.status.busy": "2026-06-07T20:53:02.425987Z", + "iopub.status.idle": "2026-06-07T20:53:03.144751Z", + "shell.execute_reply": "2026-06-07T20:53:03.143174Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -60,7 +79,14 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.147263Z", + "iopub.status.busy": "2026-06-07T20:53:03.147093Z", + "iopub.status.idle": "2026-06-07T20:53:03.283962Z", + "shell.execute_reply": "2026-06-07T20:53:03.283169Z" + } + }, "outputs": [ { "data": { @@ -68,37 +94,19 @@ "[app:\n", " name: SD_Tensile_Test_Pipeline,\n", " app:\n", - " name: ckan-fetch,\n", - " app:\n", " name: csv_tensile_test,\n", " app:\n", " name: csv_tensile_test_f2,\n", " app:\n", - " name: csv_tensile_test_three_directions,\n", - " app:\n", " name: dsms-materialcard,\n", " app:\n", " name: dsms-tensile-test-analysis,\n", " app:\n", " name: excel_notch_tensile_test,\n", " app:\n", - " name: excel_notched_tensile_test,\n", - " app:\n", " name: excel_shear_tensile_test,\n", " app:\n", - " name: excel_shear_test,\n", - " app:\n", - " name: excel_tensile_test,\n", - " app:\n", - " name: ternary-plot,\n", - " app:\n", - " name: testapp2,\n", - " app:\n", - " name: upload_double_ring_bending_test_dat,\n", - " app:\n", - " name: upload_double_ring_bending_test_data,\n", - " app:\n", - " name: upload_pilot-1_melt-spinning]" + " name: excel_tensile_test]" ] }, "execution_count": 3, @@ -143,7 +151,14 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.312124Z", + "iopub.status.busy": "2026-06-07T20:53:03.311876Z", + "iopub.status.idle": "2026-06-07T20:53:03.314942Z", + "shell.execute_reply": "2026-06-07T20:53:03.314336Z" + } + }, "outputs": [], "source": [ "data = \"\"\"A,B,C\n", @@ -170,7 +185,14 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.316738Z", + "iopub.status.busy": "2026-06-07T20:53:03.316596Z", + "iopub.status.idle": "2026-06-07T20:53:03.319128Z", + "shell.execute_reply": "2026-06-07T20:53:03.318397Z" + } + }, "outputs": [], "source": [ "configname = \"testapp2\"" @@ -202,7 +224,14 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.320714Z", + "iopub.status.busy": "2026-06-07T20:53:03.320572Z", + "iopub.status.idle": "2026-06-07T20:53:03.323949Z", + "shell.execute_reply": "2026-06-07T20:53:03.323126Z" + } + }, "outputs": [], "source": [ "parameters = [\n", @@ -245,7 +274,14 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.325519Z", + "iopub.status.busy": "2026-06-07T20:53:03.325368Z", + "iopub.status.idle": "2026-06-07T20:53:03.328220Z", + "shell.execute_reply": "2026-06-07T20:53:03.327423Z" + } + }, "outputs": [], "source": [ "# Define app specification\n", @@ -271,7 +307,14 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.329696Z", + "iopub.status.busy": "2026-06-07T20:53:03.329518Z", + "iopub.status.idle": "2026-06-07T20:53:03.332138Z", + "shell.execute_reply": "2026-06-07T20:53:03.331496Z" + } + }, "outputs": [], "source": [ "appspec = AppConfig(\n", @@ -298,13 +341,20 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.333580Z", + "iopub.status.busy": "2026-06-07T20:53:03.333444Z", + "iopub.status.idle": "2026-06-07T20:53:03.414177Z", + "shell.execute_reply": "2026-06-07T20:53:03.413221Z" + } + }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/app/dsms/apps/config.py:86: UserWarning: AppConfigs do not have a refresh functionality since they are already up to date after committing. You can continue normally using the app config.\n", + "/root/dsms/dsms-python-sdk/dsms/apps/config.py:86: UserWarning: AppConfigs do not have a refresh functionality since they are already up to date after committing. You can continue normally using the app config.\n", " warnings.warn(\n" ] } @@ -324,7 +374,14 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.415816Z", + "iopub.status.busy": "2026-06-07T20:53:03.415668Z", + "iopub.status.idle": "2026-06-07T20:53:03.419781Z", + "shell.execute_reply": "2026-06-07T20:53:03.418974Z" + } + }, "outputs": [ { "data": { @@ -342,7 +399,7 @@ " 'value': '\\n [\\n {\\n \"key\": \"A\",\\n \"iri\": \"https://w3id.org/steel/ProcessOntology/TestTime\",\\n \"unit\": \"s\"\\n },\\n {\\n \"key\": \"B\",\\n \"iri\": \"https://w3id.org/steel/ProcessOntology/StandardForce\",\\n \"unit\": \"kN\"\\n },\\n {\\n \"key\": \"C\",\\n \"iri\": \"https://w3id.org/steel/ProcessOntology/AbsoluteCrossheadTravel\",\\n \"unit\": \"mm\"\\n }\\n ]\\n '},\n", " {'name': 'request_timeout', 'value': 120},\n", " {'name': 'ping', 'value': True},\n", - " {'name': 'host_url', 'value': 'https://bue.materials-data.space/'},\n", + " {'name': 'host_url', 'value': 'https://nash.materials-data.space/'},\n", " {'name': 'ssl_verify', 'value': True},\n", " {'name': 'kitem_repo', 'value': 'knowledge-items'},\n", " {'name': 'encoding', 'value': 'utf-8'}]}}}" @@ -361,13 +418,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we would like to apply the app config to a KItem. `triggerUponUpload` must be set to `True` so that the app is triggered automatically when we upload an attachment.\n\nAdditionally, we must tell the file extension for which the upload shall be triggered. Here it is `.csv`.\n\nWe also want to generate a qr code as avatar for the KItem with `avatar={\"include_qr\": True}`." + "Now we would like to apply the app config to a KItem. `triggerUponUpload` must be set to `True` so that the app is triggered automatically when we upload an attachment.\n", + "\n", + "Additionally, we must tell the file extension for which the upload shall be triggered. Here it is `.csv`.\n", + "\n", + "We also want to generate a qr code as avatar for the KItem with `avatar={\"include_qr\": True}`." ] }, { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.421329Z", + "iopub.status.busy": "2026-06-07T20:53:03.421162Z", + "iopub.status.idle": "2026-06-07T20:53:03.628931Z", + "shell.execute_reply": "2026-06-07T20:53:03.628038Z" + } + }, "outputs": [], "source": [ "item = KItem(\n", @@ -397,7 +465,14 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:03.631192Z", + "iopub.status.busy": "2026-06-07T20:53:03.631039Z", + "iopub.status.idle": "2026-06-07T20:53:05.676407Z", + "shell.execute_reply": "2026-06-07T20:53:05.675188Z" + } + }, "outputs": [], "source": [ "dsms.add(item)\n", @@ -414,7 +489,14 @@ { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:05.679141Z", + "iopub.status.busy": "2026-06-07T20:53:05.678966Z", + "iopub.status.idle": "2026-06-07T20:53:05.682208Z", + "shell.execute_reply": "2026-06-07T20:53:05.681416Z" + } + }, "outputs": [], "source": [ "item.attachments = [{\"name\": \"dummy_data.csv\", \"content\": data}]" @@ -430,7 +512,14 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:05.683976Z", + "iopub.status.busy": "2026-06-07T20:53:05.683832Z", + "iopub.status.idle": "2026-06-07T20:53:07.731410Z", + "shell.execute_reply": "2026-06-07T20:53:07.730203Z" + } + }, "outputs": [], "source": [ "dsms.add(item)\n", @@ -461,7 +550,14 @@ { "cell_type": "code", "execution_count": 15, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:07.734091Z", + "iopub.status.busy": "2026-06-07T20:53:07.733915Z", + "iopub.status.idle": "2026-06-07T20:53:08.578198Z", + "shell.execute_reply": "2026-06-07T20:53:08.577314Z" + } + }, "outputs": [], "source": [ "item.refresh()" @@ -470,29 +566,35 @@ { "cell_type": "code", "execution_count": 16, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:08.580406Z", + "iopub.status.busy": "2026-06-07T20:53:08.580249Z", + "iopub.status.idle": "2026-06-07T20:53:08.584714Z", + "shell.execute_reply": "2026-06-07T20:53:08.583958Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "kitem:\n", - " id: fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\n", + " id: 2ef6d03a-099e-40c4-a508-c90b5c85789f\n", " name: my tensile test experiment\n", " ktype_id: dataset\n", - " slug: mytensiletestexperiment-fe51bac4\n", + " slug: mytensiletestexperiment-2ef6d03a\n", + " avatar_exists: false\n", + " has_contexts: false\n", " annotations: []\n", " attachments:\n", " - name: dummy_data.csv\n", - " - name: subgraph.ttl\n", " linked_kitems: []\n", " affiliations: []\n", - " authors:\n", - " - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\n", - " avatar_exists: false\n", + " authors: []\n", " contacts: []\n", - " created_at: 2025-08-14 15:44:01.267904\n", - " updated_at: 2025-08-14 15:44:01.267904\n", + " created_at: 2026-06-07 20:53:04.030500\n", + " updated_at: 2026-06-07 20:53:04.030500\n", " external_links: []\n", " apps:\n", " - executable: testapp2\n", @@ -503,20 +605,13 @@ " triggerUponUpload: true\n", " triggerUponUploadFileExtensions:\n", " - .csv\n", - " user_groups: []\n", - " dataframe:\n", - " - id: &id001 !!python/object:uuid.UUID\n", - " int: 338048275090092658041595342616544333210\n", - " is_safe: 0\n", - " column_id: 0\n", - " name: TestTime\n", - " - id: *id001\n", - " column_id: 1\n", - " name: StandardForce\n", - " - id: *id001\n", - " column_id: 2\n", - " name: AbsoluteCrossheadTravel\n", - " rdf_exists: true\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", " contexts: []\n", "\n" ] @@ -536,62 +631,21 @@ { "cell_type": "code", "execution_count": 17, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:08.586066Z", + "iopub.status.busy": "2026-06-07T20:53:08.585936Z", + "iopub.status.idle": "2026-06-07T20:53:08.669524Z", + "shell.execute_reply": "2026-06-07T20:53:08.668738Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "@prefix csvw: .\n", - "@prefix dcat: .\n", - "@prefix dcterms: .\n", - "@prefix ns1: .\n", - "@prefix ns2: .\n", - "@prefix rdfs: .\n", - "@prefix xsd: .\n", - "\n", - " a dcat:Dataset ;\n", - " dcterms:hasPart ;\n", - " dcat:distribution [ a dcat:Distribution ;\n", - " dcat:accessURL \"https://bue.materials-data.space/api/knowledge/data_api/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\"^^xsd:anyURI ;\n", - " dcat:mediaType \"http://www.iana.org/assignments/media-types/text/csv\"^^xsd:anyURI ] .\n", - "\n", - " a ;\n", - " ns1:hasUnit \"http://qudt.org/vocab/unit/MilliM\"^^xsd:anyURI .\n", - "\n", - " a ;\n", - " ns1:hasUnit \"http://qudt.org/vocab/unit/KiloN\"^^xsd:anyURI .\n", - "\n", - " a ;\n", - " ns1:hasUnit \"http://qudt.org/vocab/unit/SEC\"^^xsd:anyURI .\n", - "\n", - " a csvw:TableGroup ;\n", - " csvw:table [ a csvw:Table ;\n", - " rdfs:label \"Dataframe\" ;\n", - " csvw:tableSchema [ a csvw:Schema ;\n", - " csvw:column [ a csvw:Column ;\n", - " ns1:quantity ;\n", - " csvw:titles \"B\"^^xsd:string ;\n", - " ns2:page [ a ns2:Document ;\n", - " dcterms:format \"https://www.iana.org/assignments/media-types/application/json\"^^xsd:anyURI ;\n", - " dcterms:identifier \"https://bue.materials-data.space/api/knowledge/data_api/column-1\"^^xsd:anyURI ;\n", - " dcterms:type \"http://purl.org/dc/terms/Dataset\"^^xsd:anyURI ] ],\n", - " [ a csvw:Column ;\n", - " ns1:quantity ;\n", - " csvw:titles \"A\"^^xsd:string ;\n", - " ns2:page [ a ns2:Document ;\n", - " dcterms:format \"https://www.iana.org/assignments/media-types/application/json\"^^xsd:anyURI ;\n", - " dcterms:identifier \"https://bue.materials-data.space/api/knowledge/data_api/column-0\"^^xsd:anyURI ;\n", - " dcterms:type \"http://purl.org/dc/terms/Dataset\"^^xsd:anyURI ] ],\n", - " [ a csvw:Column ;\n", - " ns1:quantity ;\n", - " csvw:titles \"C\"^^xsd:string ;\n", - " ns2:page [ a ns2:Document ;\n", - " dcterms:format \"https://www.iana.org/assignments/media-types/application/json\"^^xsd:anyURI ;\n", - " dcterms:identifier \"https://bue.materials-data.space/api/knowledge/data_api/column-2\"^^xsd:anyURI ;\n", - " dcterms:type \"http://purl.org/dc/terms/Dataset\"^^xsd:anyURI ] ] ] ] .\n", - "\n", - "\n" + "Note: RDF subgraph is generated asynchronously.\n", + "It may not be available immediately after creation.\n" ] } ], @@ -613,26 +667,22 @@ { "cell_type": "code", "execution_count": 18, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:08.671187Z", + "iopub.status.busy": "2026-06-07T20:53:08.671043Z", + "iopub.status.idle": "2026-06-07T20:53:08.674032Z", + "shell.execute_reply": "2026-06-07T20:53:08.673304Z" + } + }, "outputs": [ { - "data": { - "text/plain": [ - "[1300.0,\n", - " 1800.0,\n", - " 2100.0,\n", - " 2600.0,\n", - " 3200.0,\n", - " 3700.0,\n", - " 4300.0,\n", - " 4800.0,\n", - " 5300.0,\n", - " 6000.0]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: dataframe may not be available yet (app processing is asynchronous).\n", + "Error: 'NoneType' object has no attribute 'StandardForce'\n" + ] } ], "source": [ @@ -660,116 +710,22 @@ { "cell_type": "code", "execution_count": 19, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:08.675424Z", + "iopub.status.busy": "2026-06-07T20:53:08.675284Z", + "iopub.status.idle": "2026-06-07T20:53:08.678212Z", + "shell.execute_reply": "2026-06-07T20:53:08.677477Z" + } + }, "outputs": [ { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
TestTimeStandardForceAbsoluteCrossheadTravel
01.21.31.5
11.71.81.9
22.02.12.3
32.52.62.8
43.03.23.4
53.63.73.9
64.14.34.4
74.74.85.0
85.25.35.5
95.86.06.1
\n", - "
" - ], - "text/plain": [ - " TestTime StandardForce AbsoluteCrossheadTravel\n", - "0 1.2 1.3 1.5\n", - "1 1.7 1.8 1.9\n", - "2 2.0 2.1 2.3\n", - "3 2.5 2.6 2.8\n", - "4 3.0 3.2 3.4\n", - "5 3.6 3.7 3.9\n", - "6 4.1 4.3 4.4\n", - "7 4.7 4.8 5.0\n", - "8 5.2 5.3 5.5\n", - "9 5.8 6.0 6.1" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: dataframe not available (asynchronous processing).\n", + "Error: 'NoneType' object has no attribute 'to_df'\n" + ] } ], "source": [ @@ -790,7 +746,14 @@ { "cell_type": "code", "execution_count": 20, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:08.679565Z", + "iopub.status.busy": "2026-06-07T20:53:08.679436Z", + "iopub.status.idle": "2026-06-07T20:53:11.059357Z", + "shell.execute_reply": "2026-06-07T20:53:11.058402Z" + } + }, "outputs": [], "source": [ "item.dataframe = {\n", @@ -812,14 +775,27 @@ { "cell_type": "code", "execution_count": 21, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:11.062637Z", + "iopub.status.busy": "2026-06-07T20:53:11.062484Z", + "iopub.status.idle": "2026-06-07T20:53:11.673789Z", + "shell.execute_reply": "2026-06-07T20:53:11.672654Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "column: TestTime ,\n", - " data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\n", + " data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "column: StandardForce ,\n", " data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]\n", "column: AbsoluteCrossheadTravel ,\n", @@ -846,7 +822,14 @@ { "cell_type": "code", "execution_count": 22, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:11.675912Z", + "iopub.status.busy": "2026-06-07T20:53:11.675760Z", + "iopub.status.idle": "2026-06-07T20:53:12.286917Z", + "shell.execute_reply": "2026-06-07T20:53:12.285870Z" + } + }, "outputs": [], "source": [ "try:\n", @@ -878,8 +861,24 @@ { "cell_type": "code", "execution_count": 23, - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.289688Z", + "iopub.status.busy": "2026-06-07T20:53:12.289542Z", + "iopub.status.idle": "2026-06-07T20:53:12.454818Z", + "shell.execute_reply": "2026-06-07T20:53:12.453675Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: app execution requires appropriate permissions.\n", + "Error: Submission was not successful: {\"detail\":{\"message\":\"Workflow could not be executed: Server returned status code 401 with message: `Unauthorized`.\"}}\n" + ] + } + ], "source": [ "try:\n", " job = item.apps.by_title[\"data2rdf\"].run(\n", @@ -902,21 +901,21 @@ { "cell_type": "code", "execution_count": 24, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.456786Z", + "iopub.status.busy": "2026-06-07T20:53:12.456633Z", + "iopub.status.idle": "2026-06-07T20:53:12.459666Z", + "shell.execute_reply": "2026-06-07T20:53:12.458829Z" + } + }, "outputs": [ { - "data": { - "text/plain": [ - "job_status:\n", - " phase: Succeeded\n", - " finished_at: 08/14/2025, 15:44:57\n", - " started_at: 08/14/2025, 15:44:37\n", - " progress: 1/1" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Job not available.\n" + ] } ], "source": [ @@ -933,17 +932,21 @@ { "cell_type": "code", "execution_count": 25, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.461198Z", + "iopub.status.busy": "2026-06-07T20:53:12.461040Z", + "iopub.status.idle": "2026-06-07T20:53:12.464020Z", + "shell.execute_reply": "2026-06-07T20:53:12.463083Z" + } + }, "outputs": [ { - "data": { - "text/plain": [ - "'\"[2025-08-14 15:44:41,262 - dsms_data2rdf.main - INFO]: Fetch KItem: \\\\n kitem:\\\\n id: fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\\\\n name: my tensile test experiment\\\\n ktype_id: dataset\\\\n slug: mytensiletestexperiment-fe51bac4\\\\n annotations: []\\\\n attachments:\\\\n - name: dummy_data.csv\\\\n - name: subgraph.ttl\\\\n linked_kitems: []\\\\n affiliations: []\\\\n authors:\\\\n - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\\\\n avatar_exists: false\\\\n contacts: []\\\\n created_at: 2025-08-14 15:44:01.267904\\\\n updated_at: 2025-08-14 15:44:01.267904\\\\n external_links: []\\\\n apps:\\\\n - executable: testapp2\\\\n title: data2rdf\\\\n description: null\\\\n tags: null\\\\n additional_properties:\\\\n triggerUponUpload: true\\\\n triggerUponUploadFileExtensions:\\\\n - .csv\\\\n user_groups: []\\\\n rdf_exists: true\\\\n contexts: []\\\\n\\\\n[2025-08-14 15:44:41,283 - dsms_data2rdf.main - INFO]: Run pipeline with the following parser arguments: {\\'metadata_sep\\': \\',\\', \\'metadata_length\\': 0, \\'time_series_sep\\': \\',\\', \\'time_series_header_length\\': 1, \\'drop_na\\': True, \\'fillna\\': None}\\\\n[2025-08-14 15:44:41,283 - dsms_data2rdf.main - INFO]: Run pipeline with the following parser: Parser.csv\\\\n[2025-08-14 15:44:41,283 - dsms_data2rdf.main - INFO]: Run pipeline with the following config: {\\'base_iri\\': \\'https://bue.materials-data.space/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\\', \\'data_download_uri\\': \\'https://bue.materials-data.space/api/knowledge/data_api/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\\', \\'graph_identifier\\': \\'https://bue.materials-data.space/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\\', \\'separator\\': \\'/\\', \\'encoding\\': \\'utf-8\\'}\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the \\'model_fields\\' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\\\n for key, value in self.model_fields.items():\\\\n[2025-08-14 15:44:46,555 - dsms_data2rdf.main - INFO]: Pipeline finished.\\\\n[2025-08-14 15:44:46,555 - dsms_data2rdf.main - INFO]: Pipeline did detect any metadata. Will not make annotations for KItem\\\\n[2025-08-14 15:44:46,555 - dsms_data2rdf.main - INFO]: Extracted Time Series: Index([\\'TestTime\\', \\'StandardForce\\', \\'AbsoluteCrossheadTravel\\'], dtype=\\'object\\')\\\\n[2025-08-14 15:44:47,150 - dsms_data2rdf.main - INFO]: Checking that dataframe is up to date.\\\\n[2025-08-14 15:44:47,150 - dsms_data2rdf.main - INFO]: Dataframe upload was successful after 0 retries.\\\\n[2025-08-14 15:44:47,150 - dsms_data2rdf.main - INFO]: Done!\\\\n\"'" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Job not available.\n" + ] } ], "source": [ @@ -960,8 +963,24 @@ { "cell_type": "code", "execution_count": 26, - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.465570Z", + "iopub.status.busy": "2026-06-07T20:53:12.465431Z", + "iopub.status.idle": "2026-06-07T20:53:12.603067Z", + "shell.execute_reply": "2026-06-07T20:53:12.602141Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: app execution requires appropriate permissions.\n", + "Error: Submission was not successful: {\"detail\":{\"message\":\"Workflow could not be executed: Server returned status code 401 with message: `Unauthorized`.\"}}\n" + ] + } + ], "source": [ "try:\n", " job = item.apps.by_title[\"data2rdf\"].run(\n", @@ -985,157 +1004,20 @@ { "cell_type": "code", "execution_count": 27, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.605096Z", + "iopub.status.busy": "2026-06-07T20:53:12.604930Z", + "iopub.status.idle": "2026-06-07T20:53:12.608610Z", + "shell.execute_reply": "2026-06-07T20:53:12.607662Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Running\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 0/1\n", - "\n", - "Current logs:\n", - "\"\"\n", - "\n", - "\n", - "job_status:\n", - " phase: Succeeded\n", - " finished_at: 08/14/2025, 15:45:18\n", - " started_at: 08/14/2025, 15:44:58\n", - " progress: 1/1\n", - "\n", - "Current logs:\n", - "\"[2025-08-14 15:45:02,348 - dsms_data2rdf.main - INFO]: Fetch KItem: \\n kitem:\\n id: fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a\\n name: my tensile test experiment\\n ktype_id: dataset\\n slug: mytensiletestexperiment-fe51bac4\\n annotations: []\\n attachments:\\n - name: dummy_data.csv\\n - name: subgraph.ttl\\n linked_kitems: []\\n affiliations: []\\n authors:\\n - user_id: 7f0e5a37-353b-4bbc-b1f1-b6ad575f562d\\n avatar_exists: false\\n contacts: []\\n created_at: 2025-08-14 15:44:01.267904\\n updated_at: 2025-08-14 15:44:01.267904\\n external_links: []\\n apps:\\n - executable: testapp2\\n title: data2rdf\\n description: null\\n tags: null\\n additional_properties:\\n triggerUponUpload: true\\n triggerUponUploadFileExtensions:\\n - .csv\\n user_groups: []\\n rdf_exists: true\\n contexts: []\\n\\n[2025-08-14 15:45:02,370 - dsms_data2rdf.main - INFO]: Run pipeline with the following parser arguments: {'metadata_sep': ',', 'metadata_length': 0, 'time_series_sep': ',', 'time_series_header_length': 1, 'drop_na': True, 'fillna': None}\\n[2025-08-14 15:45:02,370 - dsms_data2rdf.main - INFO]: Run pipeline with the following parser: Parser.csv\\n[2025-08-14 15:45:02,370 - dsms_data2rdf.main - INFO]: Run pipeline with the following config: {'base_iri': 'https://bue.materials-data.space/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a', 'data_download_uri': 'https://bue.materials-data.space/api/knowledge/data_api/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a', 'graph_identifier': 'https://bue.materials-data.space/fe51bac4-bc4d-4067-bcf4-60c2cd0acd9a', 'separator': '/', 'encoding': 'utf-8'}\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n/usr/local/lib/python3.10/site-packages/data2rdf/config.py:87: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\\n for key, value in self.model_fields.items():\\n[2025-08-14 15:45:07,804 - dsms_data2rdf.main - INFO]: Pipeline finished.\\n[2025-08-14 15:45:07,804 - dsms_data2rdf.main - INFO]: Pipeline did detect any metadata. Will not make annotations for KItem\\n[2025-08-14 15:45:07,804 - dsms_data2rdf.main - INFO]: Extracted Time Series: Index(['TestTime', 'StandardForce', 'AbsoluteCrossheadTravel'], dtype='object')\\n[2025-08-14 15:45:08,343 - dsms_data2rdf.main - INFO]: Checking that dataframe is up to date.\\n[2025-08-14 15:45:08,343 - dsms_data2rdf.main - INFO]: Dataframe upload was successful after 0 retries.\\n[2025-08-14 15:45:08,344 - dsms_data2rdf.main - INFO]: Done!\\n\"\n", - "\n", - "\n" + "Job not available.\n" ] } ], @@ -1164,7 +1046,14 @@ { "cell_type": "code", "execution_count": 28, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:12.610159Z", + "iopub.status.busy": "2026-06-07T20:53:12.610017Z", + "iopub.status.idle": "2026-06-07T20:53:13.611169Z", + "shell.execute_reply": "2026-06-07T20:53:13.610319Z" + } + }, "outputs": [], "source": [ "item.refresh()" @@ -1180,7 +1069,14 @@ { "cell_type": "code", "execution_count": 29, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:13.614568Z", + "iopub.status.busy": "2026-06-07T20:53:13.614407Z", + "iopub.status.idle": "2026-06-07T20:53:14.108247Z", + "shell.execute_reply": "2026-06-07T20:53:14.107482Z" + } + }, "outputs": [], "source": [ "del dsms[item]\n", @@ -1205,7 +1101,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/7_ktypes.ipynb b/docs/dsms_sdk/tutorials/7_ktypes.ipynb index 6ea57a0..ce1ce3f 100644 --- a/docs/dsms_sdk/tutorials/7_ktypes.ipynb +++ b/docs/dsms_sdk/tutorials/7_ktypes.ipynb @@ -11,13 +11,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 7.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nImport the needed classes and functions." + "## 7.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "Import the needed classes and functions." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:17.628536Z", + "iopub.status.busy": "2026-06-07T20:53:17.628382Z", + "iopub.status.idle": "2026-06-07T20:53:18.276463Z", + "shell.execute_reply": "2026-06-07T20:53:18.275473Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KType" @@ -32,8 +43,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:18.279758Z", + "iopub.status.busy": "2026-06-07T20:53:18.279447Z", + "iopub.status.idle": "2026-06-07T20:53:18.785321Z", + "shell.execute_reply": "2026-06-07T20:53:18.784288Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -51,23 +69,129 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 7.2. Create KTypes\n\nA KType can have a webform schema - for e.g. data properties or a process schema for clustering KItems into a context.\n\nThe webform and process schema of a ktype may look as follows:" + "## 7.2. Create KTypes\n", + "\n", + "A KType can have a webform schema - for e.g. data properties or a process schema for clustering KItems into a context.\n", + "\n", + "The webform and process schema of a ktype may look as follows:" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:18.788399Z", + "iopub.status.busy": "2026-06-07T20:53:18.788212Z", + "iopub.status.idle": "2026-06-07T20:53:18.794850Z", + "shell.execute_reply": "2026-06-07T20:53:18.793893Z" + } + }, "outputs": [], "source": [ - "ktype = {\n \"id\": \"sdk-tutorial\",\n \"name\": \"SDK Tutorial\",\n \"webform_schema_id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n \"webform_schema\": {\n \"id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n \"name\": \"SDK Tutorial\",\n \"spec\": {\n \"semantics_enabled\": True,\n \"sections_enabled\": False,\n \"class_mapping\": [\n \"https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\"\n ],\n \"sections\": [\n {\n \"id\": \"id57f43923129f28\",\n \"name\": \"Untitled Section\",\n \"inputs\": [\n {\n \"id\": \"id02760f0b0cd56\",\n \"label\": \"Start time\",\n \"widget\": \"Text\",\n \"relation_mapping\": {\n \"iri\": \"http://www.w3.org/ns/dcat#startDate\",\n \"label\": \"start date\",\n \"type\": \"data_property\",\n },\n },\n {\n \"id\": \"id3ff5961015588\",\n \"label\": \"End time\",\n \"widget\": \"Text\",\n \n \"relation_mapping\": {\n \"iri\": \"http://www.w3.org/ns/dcat#endDate\",\n \"label\": \"end date\",\n \"type\": \"data_property\",\n },\n }\n ],\n }\n ]\n },\n },\n \"process_schema_id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n \"process_schema\": {\n \"id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n \"name\": \"SDK Tutorial\",\n \"spec\": [\n {\n \"id\": \"specimen\",\n \"label\": \"Specimen\",\n },\n {\n \"id\": \"testingmachine\",\n \"label\": \"TestingMachine\",\n },\n {\n \"id\": \"expert\",\n \"label\": \"expert\",\n \"mappings\": [\n {\n \"dst_ktype_id\": \"organization\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n },\n {\n \"dst_ktype_id\": \"testingmachine\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n },\n {\n \"dst_ktype_id\": \"specimen\",\n \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n \"relation_name\": \"wasAssociatedWith\"\n }\n ],\n },\n {\n \"id\": \"organization\",\n \"label\": \"organization\",\n }\n ],\n },\n}" + "ktype = {\n", + " \"id\": \"sdk-tutorial\",\n", + " \"name\": \"SDK Tutorial\",\n", + " \"webform_schema_id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n", + " \"webform_schema\": {\n", + " \"id\": \"69e58442-5837-4bba-9326-3083bccc7c86\",\n", + " \"name\": \"SDK Tutorial\",\n", + " \"spec\": {\n", + " \"semantics_enabled\": True,\n", + " \"sections_enabled\": False,\n", + " \"class_mapping\": [\n", + " \"https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\"\n", + " ],\n", + " \"sections\": [\n", + " {\n", + " \"id\": \"id57f43923129f28\",\n", + " \"name\": \"Untitled Section\",\n", + " \"inputs\": [\n", + " {\n", + " \"id\": \"id02760f0b0cd56\",\n", + " \"label\": \"Start time\",\n", + " \"widget\": \"Text\",\n", + " \"relation_mapping\": {\n", + " \"iri\": \"http://www.w3.org/ns/dcat#startDate\",\n", + " \"label\": \"start date\",\n", + " \"type\": \"data_property\",\n", + " },\n", + " },\n", + " {\n", + " \"id\": \"id3ff5961015588\",\n", + " \"label\": \"End time\",\n", + " \"widget\": \"Text\",\n", + " \n", + " \"relation_mapping\": {\n", + " \"iri\": \"http://www.w3.org/ns/dcat#endDate\",\n", + " \"label\": \"end date\",\n", + " \"type\": \"data_property\",\n", + " },\n", + " }\n", + " ],\n", + " }\n", + " ]\n", + " },\n", + " },\n", + " \"process_schema_id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n", + " \"process_schema\": {\n", + " \"id\": \"ee815110-ee41-44cf-a049-5873942440d6\",\n", + " \"name\": \"SDK Tutorial\",\n", + " \"spec\": [\n", + " {\n", + " \"id\": \"specimen\",\n", + " \"label\": \"Specimen\",\n", + " },\n", + " {\n", + " \"id\": \"testingmachine\",\n", + " \"label\": \"TestingMachine\",\n", + " },\n", + " {\n", + " \"id\": \"expert\",\n", + " \"label\": \"expert\",\n", + " \"mappings\": [\n", + " {\n", + " \"dst_ktype_id\": \"organization\",\n", + " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", + " \"relation_name\": \"wasAssociatedWith\"\n", + " },\n", + " {\n", + " \"dst_ktype_id\": \"testingmachine\",\n", + " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", + " \"relation_name\": \"wasAssociatedWith\"\n", + " },\n", + " {\n", + " \"dst_ktype_id\": \"specimen\",\n", + " \"relation_iri\": \"http://www.w3.org/ns/prov#wasAssociatedWith\",\n", + " \"relation_name\": \"wasAssociatedWith\"\n", + " }\n", + " ],\n", + " },\n", + " {\n", + " \"id\": \"organization\",\n", + " \"label\": \"organization\",\n", + " }\n", + " ],\n", + " },\n", + "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can see, that the ktype is describing a SDK tutorial KType.\nThe webform describes two fields:\n* start date of the characterization (with dcat mapping)\n* end date of the characterization (with dcat mapping)\n\nThe process schema describes the following entities:\n\n* the expert involved\n* the testing machine involved\n* the specimen involved\n* the organization involved\n* the semantic relations between the expert and the organization/specimen/testing machine" + "We can see, that the ktype is describing a SDK tutorial KType.\n", + "The webform describes two fields:\n", + "* start date of the characterization (with dcat mapping)\n", + "* end date of the characterization (with dcat mapping)\n", + "\n", + "The process schema describes the following entities:\n", + "\n", + "* the expert involved\n", + "* the testing machine involved\n", + "* the specimen involved\n", + "* the organization involved\n", + "* the semantic relations between the expert and the organization/specimen/testing machine" ] }, { @@ -79,9 +203,79 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:18.796972Z", + "iopub.status.busy": "2026-06-07T20:53:18.796817Z", + "iopub.status.idle": "2026-06-07T20:53:18.805316Z", + "shell.execute_reply": "2026-06-07T20:53:18.804308Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ktype:\n", + " id: sdk-tutorial\n", + " name: SDK Tutorial\n", + " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " webform_schema:\n", + " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " name: SDK Tutorial\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", + " sections:\n", + " - id: id57f43923129f28\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id02760f0b0cd56\n", + " label: Start time\n", + " widget: Text\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#startDate\n", + " label: start date\n", + " type: data_property\n", + " - id: id3ff5961015588\n", + " label: End time\n", + " widget: Text\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#endDate\n", + " label: end date\n", + " type: data_property\n", + " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", + " process_schema:\n", + " id: ee815110-ee41-44cf-a049-5873942440d6\n", + " name: SDK Tutorial\n", + " spec:\n", + " - id: specimen\n", + " label: Specimen\n", + " - id: testingmachine\n", + " label: TestingMachine\n", + " - id: expert\n", + " label: expert\n", + " mappings:\n", + " - dst_ktype_id: organization\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " - dst_ktype_id: testingmachine\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " - dst_ktype_id: specimen\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " - id: organization\n", + " label: organization" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "ktype = KType(**ktype)\n", "\n", @@ -97,8 +291,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:18.833134Z", + "iopub.status.busy": "2026-06-07T20:53:18.832969Z", + "iopub.status.idle": "2026-06-07T20:53:19.870473Z", + "shell.execute_reply": "2026-06-07T20:53:19.869037Z" + } + }, "outputs": [], "source": [ "dsms.add(ktype)\n", @@ -114,9 +315,136 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:19.873127Z", + "iopub.status.busy": "2026-06-07T20:53:19.872947Z", + "iopub.status.idle": "2026-06-07T20:53:19.882662Z", + "shell.execute_reply": "2026-06-07T20:53:19.881381Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/root/semantic-dataspace/.venv/lib/python3.11/site-packages/pydantic/main.py:475: UserWarning: Pydantic serializer warnings:\n", + " PydanticSerializationUnexpectedValue(Expected `WebformSchema` - serialized value may not be as expected [field_name='webform_schema', input_value={'id': '69e58442-5837-4bb...-06-05T15:51:35.791107'}, input_type=dict])\n", + " PydanticSerializationUnexpectedValue(Expected `ProcessSchema` - serialized value may not be as expected [field_name='process_schema', input_value={'id': 'ee815110-ee41-44c...-06-05T15:51:35.927615'}, input_type=dict])\n", + " return self.__pydantic_serializer__.to_python(\n" + ] + }, + { + "data": { + "text/plain": [ + "ktype:\n", + " id: sdk-tutorial\n", + " name: SDK Tutorial\n", + " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " webform_schema:\n", + " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " name: SDK Tutorial\n", + " spec:\n", + " semanticsEnabled: true\n", + " sectionsEnabled: false\n", + " classMapping:\n", + " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", + " sections:\n", + " - id: id57f43923129f28\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id02760f0b0cd56\n", + " label: Start time\n", + " widget: Text\n", + " required: false\n", + " value: null\n", + " hint: null\n", + " hidden: false\n", + " ignore: false\n", + " selectOptions: []\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#startDate\n", + " label: start date\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " relationMappingExtra: null\n", + " multipleSelection: false\n", + " knowledgeType: null\n", + " rangeOptions: null\n", + " placeholder: null\n", + " - id: id3ff5961015588\n", + " label: End time\n", + " widget: Text\n", + " required: false\n", + " value: null\n", + " hint: null\n", + " hidden: false\n", + " ignore: false\n", + " selectOptions: []\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#endDate\n", + " label: end date\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " relationMappingExtra: null\n", + " multipleSelection: false\n", + " knowledgeType: null\n", + " rangeOptions: null\n", + " placeholder: null\n", + " hidden: false\n", + " description: null\n", + " created_at: '2026-06-05T15:51:35.791107'\n", + " updated_at: '2026-06-05T15:51:35.791107'\n", + " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", + " process_schema:\n", + " id: ee815110-ee41-44cf-a049-5873942440d6\n", + " name: SDK Tutorial\n", + " spec:\n", + " - id: specimen\n", + " label: Specimen\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " - id: testingmachine\n", + " label: TestingMachine\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " - id: expert\n", + " label: expert\n", + " isChild: false\n", + " mappings:\n", + " - dstKtypeId: organization\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " - dstKtypeId: testingmachine\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " - dstKtypeId: specimen\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " children: []\n", + " - id: organization\n", + " label: organization\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " created_at: '2026-06-05T15:51:35.927615'\n", + " updated_at: '2026-06-05T15:51:35.927615'\n", + " created_at: '2026-06-07T20:53:19.366066'\n", + " updated_at: '2026-06-07T20:53:19.440531'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "ktype" ] @@ -137,11 +465,112 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:19.884494Z", + "iopub.status.busy": "2026-06-07T20:53:19.884339Z", + "iopub.status.idle": "2026-06-07T20:53:19.892012Z", + "shell.execute_reply": "2026-06-07T20:53:19.890973Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ktype:\n", + " id: sdk-tutorial\n", + " name: SDK Tutorial\n", + " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " webform_schema:\n", + " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " name: SDK Tutorial\n", + " spec:\n", + " semantics_enabled: true\n", + " sections_enabled: false\n", + " class_mapping:\n", + " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", + " sections:\n", + " - id: id57f43923129f28\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id02760f0b0cd56\n", + " label: Start time\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#startDate\n", + " label: start date\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " - id: id3ff5961015588\n", + " label: End time\n", + " widget: Text\n", + " required: false\n", + " hidden: false\n", + " ignore: false\n", + " select_options: []\n", + " relation_mapping:\n", + " iri: http://www.w3.org/ns/dcat#endDate\n", + " label: end date\n", + " type: data_property\n", + " inverse: false\n", + " multiple_selection: false\n", + " hidden: false\n", + " created_at: '2026-06-05T15:51:35.791107'\n", + " updated_at: '2026-06-05T15:51:35.791107'\n", + " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", + " process_schema:\n", + " id: ee815110-ee41-44cf-a049-5873942440d6\n", + " name: SDK Tutorial\n", + " spec:\n", + " - id: specimen\n", + " label: Specimen\n", + " is_child: false\n", + " mappings: []\n", + " children: []\n", + " - id: testingmachine\n", + " label: TestingMachine\n", + " is_child: false\n", + " mappings: []\n", + " children: []\n", + " - id: expert\n", + " label: expert\n", + " is_child: false\n", + " mappings:\n", + " - dst_ktype_id: organization\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " - dst_ktype_id: testingmachine\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " - dst_ktype_id: specimen\n", + " relation_iri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relation_name: wasAssociatedWith\n", + " children: []\n", + " - id: organization\n", + " label: organization\n", + " is_child: false\n", + " mappings: []\n", + " children: []\n", + " created_at: 2026-06-05 15:51:35.927615\n", + " updated_at: 2026-06-05 15:51:35.927615\n", + " created_at: '2026-06-07T20:53:19.366066'\n", + " updated_at: '2026-06-07T20:53:19.440531'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "ktype = dsms.ktypes.SdkTutorial\nktype" + "ktype = dsms.ktypes.SdkTutorial\n", + "ktype" ] }, { @@ -153,11 +582,133 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:19.893766Z", + "iopub.status.busy": "2026-06-07T20:53:19.893608Z", + "iopub.status.idle": "2026-06-07T20:53:20.814665Z", + "shell.execute_reply": "2026-06-07T20:53:20.813498Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ktype:\n", + " id: sdk-tutorial\n", + " name: SDK Tutorial Updated\n", + " webform_schema_id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " webform_schema:\n", + " id: 69e58442-5837-4bba-9326-3083bccc7c86\n", + " name: SDK Tutorial\n", + " spec:\n", + " semanticsEnabled: true\n", + " sectionsEnabled: false\n", + " classMapping:\n", + " - https://w3id.org/emmo/domain/characterisation-methodology/chameo#CharacterisationProcedure\n", + " sections:\n", + " - id: id57f43923129f28\n", + " name: Untitled Section\n", + " inputs:\n", + " - id: id02760f0b0cd56\n", + " label: Start time\n", + " widget: Text\n", + " required: false\n", + " value: null\n", + " hint: null\n", + " hidden: false\n", + " ignore: false\n", + " selectOptions: []\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#startDate\n", + " label: start date\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " relationMappingExtra: null\n", + " multipleSelection: false\n", + " knowledgeType: null\n", + " rangeOptions: null\n", + " placeholder: null\n", + " - id: id3ff5961015588\n", + " label: End time\n", + " widget: Text\n", + " required: false\n", + " value: null\n", + " hint: null\n", + " hidden: false\n", + " ignore: false\n", + " selectOptions: []\n", + " measurementUnit: null\n", + " relationMapping:\n", + " iri: http://www.w3.org/ns/dcat#endDate\n", + " label: end date\n", + " type: data_property\n", + " classIri: null\n", + " inverse: false\n", + " relationMappingExtra: null\n", + " multipleSelection: false\n", + " knowledgeType: null\n", + " rangeOptions: null\n", + " placeholder: null\n", + " hidden: false\n", + " description: null\n", + " created_at: '2026-06-05T15:51:35.791107'\n", + " updated_at: '2026-06-05T15:51:35.791107'\n", + " process_schema_id: ee815110-ee41-44cf-a049-5873942440d6\n", + " process_schema:\n", + " id: ee815110-ee41-44cf-a049-5873942440d6\n", + " name: SDK Tutorial\n", + " spec:\n", + " - id: specimen\n", + " label: Specimen\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " - id: testingmachine\n", + " label: TestingMachine\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " - id: expert\n", + " label: expert\n", + " isChild: false\n", + " mappings:\n", + " - dstKtypeId: organization\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " - dstKtypeId: testingmachine\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " - dstKtypeId: specimen\n", + " relationIri: http://www.w3.org/ns/prov#wasAssociatedWith\n", + " relationName: wasAssociatedWith\n", + " children: []\n", + " - id: organization\n", + " label: organization\n", + " isChild: false\n", + " mappings: []\n", + " children: []\n", + " created_at: '2026-06-05T15:51:35.927615'\n", + " updated_at: '2026-06-05T15:51:35.927615'\n", + " created_at: '2026-06-07T20:53:19.366066'\n", + " updated_at: '2026-06-07T20:53:20.394140'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "ktype.name = 'SDK Tutorial Updated'\n\ndsms.add(ktype)\ndsms.commit()\n\nktype" + "ktype.name = 'SDK Tutorial Updated'\n", + "\n", + "dsms.add(ktype)\n", + "dsms.commit()\n", + "\n", + "ktype" ] }, { @@ -176,8 +727,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:20.816888Z", + "iopub.status.busy": "2026-06-07T20:53:20.816716Z", + "iopub.status.idle": "2026-06-07T20:53:20.819828Z", + "shell.execute_reply": "2026-06-07T20:53:20.818820Z" + } + }, "outputs": [], "source": [ "del dsms[ktype]" @@ -192,8 +750,15 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:20.821534Z", + "iopub.status.busy": "2026-06-07T20:53:20.821388Z", + "iopub.status.idle": "2026-06-07T20:53:24.727818Z", + "shell.execute_reply": "2026-06-07T20:53:24.726720Z" + } + }, "outputs": [], "source": [ "dsms.commit()" @@ -203,100 +768,312 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 7.5. KType v2: Semantic Specifications\n\nKType v2 extends the base `KType` with a semantic specification (`KTypeSpec`) that captures ontology classes, relations, schema references, inheritance, and versioning. The v2 endpoints are available via `DSMS.get_v2_ktypes()` and related methods." + "## 7.5. KType v2: Semantic Specifications\n", + "\n", + "KType v2 extends the base `KType` with a semantic specification (`KTypeSpec`) that captures ontology classes, relations, schema references, inheritance, and versioning. The v2 endpoints are available via `DSMS.get_v2_ktypes()` and related methods." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.1. Listing v2 KTypes\n\n`DSMS.get_v2_ktypes()` returns a list of `KTypeV2` objects. Each has an optional `spec` attribute of type `KTypeSpec`. KTypes that were created through the legacy v1 endpoint have `spec=None`." + "### 7.5.1. Listing v2 KTypes\n", + "\n", + "`DSMS.get_v2_ktypes()` returns a list of `KTypeV2` objects. Each has an optional `spec` attribute of type `KTypeSpec`. KTypes that were created through the legacy v1 endpoint have `spec=None`." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:24.731559Z", + "iopub.status.busy": "2026-06-07T20:53:24.731380Z", + "iopub.status.idle": "2026-06-07T20:53:24.849401Z", + "shell.execute_reply": "2026-06-07T20:53:24.848378Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset-catalog | spec: False\n", + "dataset | spec: False\n", + "chemical-element | spec: False\n", + "engineered-raw-material | spec: False\n", + "app | spec: True\n", + "production-process | spec: False\n", + "specimen | spec: True\n", + "material-model | spec: False\n", + "magnetic-material | spec: False\n", + "srap | spec: False\n", + "production-process-chain | spec: False\n", + "engineered-material | spec: False\n", + "characterization-process | spec: True\n", + "act | spec: False\n", + "production-device | spec: False\n", + "emmc-workshop | spec: False\n", + "silver-palladium-alloy | spec: False\n", + "raw-material | spec: False\n", + "project | spec: True\n", + "document | spec: False\n", + "web-ressource | spec: False\n", + "measurement-process | spec: False\n", + "standard | spec: False\n", + "external-data-source | spec: False\n", + "aluminium-alloy | spec: False\n", + "carbonfootprint | spec: False\n", + "address | spec: False\n", + "manufacturing-process | spec: False\n", + "expert | spec: True\n", + "semi-finished-product | spec: False\n", + "raw-material-profile | spec: False\n", + "tensile-test | spec: True\n", + "organization | spec: False\n", + "material | spec: True\n", + "dasda | spec: False\n", + "process | spec: False\n", + "chemical-composition | spec: False\n", + "measurement-device | spec: False\n", + "material-card | spec: False\n", + "person | spec: False\n", + "creep-specimen | spec: True\n", + "flat-specimen | spec: True\n", + "metal-sheet-batch | spec: True\n", + "specimen-batch | spec: True\n", + "batch | spec: True\n", + "metal-sheet | spec: True\n" + ] + } + ], "source": [ - "from dsms.knowledge.ktype import KTypeV2\n\nv2_ktypes = dsms.get_v2_ktypes()\nfor kt in v2_ktypes:\n print(kt.id, \"| spec:\", kt.spec is not None)" + "from dsms.knowledge.ktype import KTypeV2\n", + "\n", + "v2_ktypes = dsms.get_v2_ktypes()\n", + "for kt in v2_ktypes:\n", + " print(kt.id, \"| spec:\", kt.spec is not None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.2. Creating a v2 KType\n\nUse `CreateKTypeRequest` to define the new KType. The `id` must be a lowercase slug (letters, digits, and hyphens only, starting with a letter)." + "### 7.5.2. Creating a v2 KType\n", + "\n", + "Use `CreateKTypeRequest` to define the new KType. The `id` must be a lowercase slug (letters, digits, and hyphens only, starting with a letter)." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:24.851662Z", + "iopub.status.busy": "2026-06-07T20:53:24.851499Z", + "iopub.status.idle": "2026-06-07T20:53:24.969612Z", + "shell.execute_reply": "2026-06-07T20:53:24.968698Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ktype:\n", + " id: tutorial-material\n", + " name: Tutorial Material\n", + " created_at: '2026-06-07T20:53:24.915028'\n", + " updated_at: '2026-06-07T20:53:24.915028'\n", + " spec:\n", + " ktype_id: tutorial-material\n", + " format_version: '0.1'\n", + " version: 1.0.0\n", + " description: A KType created in the tutorial.\n", + " abstract: false\n", + " context: false\n", + " ontology_classes:\n", + " - iri: https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " label: Material\n", + " ontology: https://emmo.info/emmo\n", + " resolved_ontology_classes:\n", + " - iri: https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " label: Material\n", + " ontology: https://emmo.info/emmo\n", + " tags:\n", + " - tutorial\n", + " - material\n", + " has_stash: false\n", + " created_at: 2026-06-07 20:53:24.915028\n", + " updated_at: 2026-06-07 20:53:24.915028\n", + "\n" + ] + } + ], "source": [ - "from dsms.knowledge.ktype import CreateKTypeRequest, OntologyClassSpec\n\nrequest = CreateKTypeRequest(\n id=\"tutorial-material\",\n name=\"Tutorial Material\",\n version=\"1.0.0\",\n description=\"A KType created in the tutorial.\",\n ontology_classes=[\n OntologyClassSpec(\n iri=\"https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\",\n label=\"Material\",\n ontology=\"https://emmo.info/emmo\",\n )\n ],\n tags=[\"tutorial\", \"material\"],\n)\n\nv2_ktype = dsms.create_v2_ktype(request)\nprint(v2_ktype)" + "from dsms.knowledge.ktype import CreateKTypeRequest, OntologyClassSpec\n", + "\n", + "request = CreateKTypeRequest(\n", + " id=\"tutorial-material\",\n", + " name=\"Tutorial Material\",\n", + " version=\"1.0.0\",\n", + " description=\"A KType created in the tutorial.\",\n", + " ontology_classes=[\n", + " OntologyClassSpec(\n", + " iri=\"https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\",\n", + " label=\"Material\",\n", + " ontology=\"https://emmo.info/emmo\",\n", + " )\n", + " ],\n", + " tags=[\"tutorial\", \"material\"],\n", + ")\n", + "\n", + "v2_ktype = dsms.create_v2_ktype(request)\n", + "print(v2_ktype)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.3. Updating a v2 KType specification\n\nUse `KTypeSpecPayload` for partial updates. Only the fields you set will be changed on the server." + "### 7.5.3. Updating a v2 KType specification\n", + "\n", + "Use `KTypeSpecPayload` for partial updates. Only the fields you set will be changed on the server." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:24.971432Z", + "iopub.status.busy": "2026-06-07T20:53:24.971271Z", + "iopub.status.idle": "2026-06-07T20:53:25.087448Z", + "shell.execute_reply": "2026-06-07T20:53:25.086658Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ktype:\n", + " id: tutorial-material\n", + " name: Tutorial Material\n", + " created_at: '2026-06-07T20:53:24.915028'\n", + " updated_at: '2026-06-07T20:53:24.915028'\n", + " spec:\n", + " ktype_id: tutorial-material\n", + " format_version: '0.1'\n", + " version: 1.0.0\n", + " description: An updated description for the tutorial material KType.\n", + " abstract: false\n", + " context: false\n", + " ontology_classes:\n", + " - iri: https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " label: Material\n", + " ontology: https://emmo.info/emmo\n", + " resolved_ontology_classes:\n", + " - iri: https://emmo.info/emmo#EMMO_4207e895_8b83_4318_996a_72cfb32acd94\n", + " label: Material\n", + " ontology: https://emmo.info/emmo\n", + " tags:\n", + " - tutorial\n", + " - material\n", + " - updated\n", + " has_stash: false\n", + " created_at: 2026-06-07 20:53:24.915028\n", + " updated_at: 2026-06-07 20:53:25.037033\n", + "\n" + ] + } + ], "source": [ - "from dsms.knowledge.ktype import KTypeSpecPayload\n\npayload = KTypeSpecPayload(\n description=\"An updated description for the tutorial material KType.\",\n tags=[\"tutorial\", \"material\", \"updated\"],\n)\n\nv2_ktype = dsms.update_v2_ktype(\"tutorial-material\", payload)\nprint(v2_ktype)" + "from dsms.knowledge.ktype import KTypeSpecPayload\n", + "\n", + "payload = KTypeSpecPayload(\n", + " description=\"An updated description for the tutorial material KType.\",\n", + " tags=[\"tutorial\", \"material\", \"updated\"],\n", + ")\n", + "\n", + "v2_ktype = dsms.update_v2_ktype(\"tutorial-material\", payload)\n", + "print(v2_ktype)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.4. Importing a v2 KType from a remote URL\n\nKType specifications hosted in a remote repository (for example a GitHub repository) can be imported directly." + "### 7.5.4. Importing a v2 KType from a remote URL\n", + "\n", + "KType specifications hosted in a remote repository (for example a GitHub repository) can be imported directly." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 14, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:25.089562Z", + "iopub.status.busy": "2026-06-07T20:53:25.089414Z", + "iopub.status.idle": "2026-06-07T20:53:25.092106Z", + "shell.execute_reply": "2026-06-07T20:53:25.091293Z" + } + }, "outputs": [], "source": [ - "# v2_ktype = dsms.import_v2_ktype(\"https://raw.githubusercontent.com/your-org/your-repo/main/ktypes/my-ktype.yaml\")\n# print(v2_ktype)" + "# v2_ktype = dsms.import_v2_ktype(\"https://raw.githubusercontent.com/your-org/your-repo/main/ktypes/my-ktype.yaml\")\n", + "# print(v2_ktype)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.5. Checking for remote updates\n\n`DSMS.get_v2_ktype_remote_diff()` compares the local KType spec against the latest version in the remote repository and reports which fields have changed." + "### 7.5.5. Checking for remote updates\n", + "\n", + "`DSMS.get_v2_ktype_remote_diff()` compares the local KType spec against the latest version in the remote repository and reports which fields have changed." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 15, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:25.094173Z", + "iopub.status.busy": "2026-06-07T20:53:25.094029Z", + "iopub.status.idle": "2026-06-07T20:53:25.096910Z", + "shell.execute_reply": "2026-06-07T20:53:25.095947Z" + } + }, "outputs": [], "source": [ - "# diff = dsms.get_v2_ktype_remote_diff(\"tutorial-material\")\n# print(\"Identical:\", diff.identical)\n# for changed in diff.changed_fields:\n# print(changed.field, \"local:\", changed.local, \"remote:\", changed.remote)" + "# diff = dsms.get_v2_ktype_remote_diff(\"tutorial-material\")\n", + "# print(\"Identical:\", diff.identical)\n", + "# for changed in diff.changed_fields:\n", + "# print(changed.field, \"local:\", changed.local, \"remote:\", changed.remote)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 7.5.6. Deleting a v2 KType\n\nDeletion is blocked if KItems of that type still exist. Delete all KItems of the type first." + "### 7.5.6. Deleting a v2 KType\n", + "\n", + "Deletion is blocked if KItems of that type still exist. Delete all KItems of the type first." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 16, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:25.098700Z", + "iopub.status.busy": "2026-06-07T20:53:25.098560Z", + "iopub.status.idle": "2026-06-07T20:53:25.353057Z", + "shell.execute_reply": "2026-06-07T20:53:25.351632Z" + } + }, "outputs": [], "source": [ "dsms.delete_v2_ktype(\"tutorial-material\")" @@ -311,11 +1088,30 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 17, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:25.356305Z", + "iopub.status.busy": "2026-06-07T20:53:25.356139Z", + "iopub.status.idle": "2026-06-07T20:53:25.359816Z", + "shell.execute_reply": "2026-06-07T20:53:25.359116Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# List all available KTypes as an enum\ndsms.ktypes" + "# List all available KTypes as an enum\n", + "dsms.ktypes" ] } ], @@ -335,7 +1131,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb b/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb index b43e4a5..850c616 100644 --- a/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb +++ b/docs/dsms_sdk/tutorials/8_kitem_contexts.ipynb @@ -4,19 +4,34 @@ "cell_type": "markdown", "id": "08e6e825", "metadata": {}, - "source": "# 8. KItem Contexts" + "source": [ + "# 8. KItem Contexts" + ] }, { "cell_type": "markdown", "id": "01266cd0", "metadata": {}, - "source": "## 8.1. Setting up\n\nBefore you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n\nImport the needed classes and functions." + "source": [ + "## 8.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your interest, along with installation of this package, and have established access to the DSMS through DSMS-SDK (refer to [Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms))\n", + "\n", + "Import the needed classes and functions." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "8e119a92", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:29.027716Z", + "iopub.status.busy": "2026-06-07T20:53:29.027557Z", + "iopub.status.idle": "2026-06-07T20:53:29.765304Z", + "shell.execute_reply": "2026-06-07T20:53:29.763850Z" + } + }, "outputs": [], "source": [ "from dsms import DSMS, KItem" @@ -32,9 +47,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "ddc6cdfa", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:29.768054Z", + "iopub.status.busy": "2026-06-07T20:53:29.767679Z", + "iopub.status.idle": "2026-06-07T20:53:30.277028Z", + "shell.execute_reply": "2026-06-07T20:53:30.275922Z" + } + }, "outputs": [], "source": [ "import os\n", @@ -45,7 +67,9 @@ "cell_type": "markdown", "id": "7206cca4", "metadata": {}, - "source": "## 8.2. Putting KItems into contexts" + "source": [ + "## 8.2. Putting KItems into contexts" + ] }, { "cell_type": "markdown", @@ -60,15 +84,32 @@ "id": "ed1d8179", "metadata": {}, "source": [ - "A KItem that is designated as a context (e.g. a `Project`) can group other KItems. A KItem joins a context by setting its `contexts` field to include the context KItem.\n\nWe will create a `Project` as the context and a `Dataset` as its member." + "A KItem that is designated as a context (e.g. a `Project`) can group other KItems. A KItem joins a context by setting its `contexts` field to include the context KItem.\n", + "\n", + "We will create a `Project` as the context and a `Dataset` as its member." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "b7dcb02f", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:30.279909Z", + "iopub.status.busy": "2026-06-07T20:53:30.279715Z", + "iopub.status.idle": "2026-06-07T20:53:32.498315Z", + "shell.execute_reply": "2026-06-07T20:53:32.496821Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://nash.materials-data.space/knowledge/project/testproject-e0654a2c\n" + ] + } + ], "source": [ "# Project is a context-capable KType (its spec has context=True)\n", "project = KItem(\n", @@ -85,15 +126,32 @@ { "cell_type": "markdown", "id": "7fb7d96c", - "source": "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control.", - "metadata": {} + "metadata": {}, + "source": [ + "> **Note:** Some fields visible in KItem outputs (`authors`, `rdf_exists`, `user_groups`) are deprecated in v5.0.0 and are no longer populated by the server. They remain in the model for backward compatibility. Use `access_properties` for access control." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "c7a41db1", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:32.500837Z", + "iopub.status.busy": "2026-06-07T20:53:32.500650Z", + "iopub.status.idle": "2026-06-07T20:53:34.900219Z", + "shell.execute_reply": "2026-06-07T20:53:34.899011Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://nash.materials-data.space/knowledge/dataset/testdataset-340d5940\n" + ] + } + ], "source": [ "# Assign the dataset to the project context via the contexts field\n", "dataset = KItem(\n", @@ -111,14 +169,40 @@ { "cell_type": "markdown", "id": "db37aaad", + "metadata": {}, "source": [ "We can verify the context relationship from both directions: checking that the dataset reports its context, and checking that a freshly-fetched dataset shows `has_contexts=True`." - ], - "metadata": {} + ] }, { "cell_type": "code", + "execution_count": 5, "id": "df636cb5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:34.902434Z", + "iopub.status.busy": "2026-06-07T20:53:34.902259Z", + "iopub.status.idle": "2026-06-07T20:53:34.905926Z", + "shell.execute_reply": "2026-06-07T20:53:34.905197Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset contexts: [kitem:\n", + " id: e0654a2c-68f7-4398-800a-bf56afd156b5\n", + " name: Test project\n", + " ktype_id: project\n", + " slug: testproject-e0654a2c\n", + " avatar_exists: false\n", + " has_contexts: false\n", + "]\n", + "Dataset has_contexts: False\n" + ] + } + ], "source": [ "# From the dataset side: which contexts does this dataset belong to?\n", "print(\"Dataset contexts:\", dataset.contexts)\n", @@ -126,10 +210,7 @@ "# Verify via a fresh fetch\n", "refreshed_dataset = dsms[str(dataset.id)]\n", "print(\"Dataset has_contexts:\", refreshed_dataset.has_contexts)" - ], - "metadata": {}, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", @@ -141,10 +222,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "640813be", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:34.907951Z", + "iopub.status.busy": "2026-06-07T20:53:34.907805Z", + "iopub.status.idle": "2026-06-07T20:53:34.913991Z", + "shell.execute_reply": "2026-06-07T20:53:34.913406Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "kitem:\n", + " id: e0654a2c-68f7-4398-800a-bf56afd156b5\n", + " name: Test project\n", + " ktype_id: project\n", + " slug: testproject-e0654a2c\n", + " avatar_exists: false\n", + " has_contexts: false\n", + " annotations: []\n", + " attachments: []\n", + " linked_kitems: []\n", + " affiliations: []\n", + " authors: []\n", + " contacts: []\n", + " created_at: 2026-06-07 20:53:30.905592\n", + " updated_at: 2026-06-07 20:53:30.905592\n", + " external_links: []\n", + " apps: []\n", + " rdf_exists: false\n", + " access_properties:\n", + " visibility: private\n", + " user_access:\n", + " - role: OWNER\n", + " user_id: 6be66d9f-1a9f-44fc-8176-f71155de06ba\n", + " group_access: []\n", + " contexts: []" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "project" ] @@ -152,37 +275,85 @@ { "cell_type": "markdown", "id": "d59e0f61", - "source": "## 8.3. Searching within a context\n\n`DSMS.search()` accepts a `contexts` parameter: a list of KItem IDs. The search returns only KItems that belong to at least one of the specified contexts.", - "metadata": {} + "metadata": {}, + "source": [ + "## 8.3. Searching within a context\n", + "\n", + "`DSMS.search()` accepts a `contexts` parameter: a list of KItem IDs. The search returns only KItems that belong to at least one of the specified contexts." + ] }, { "cell_type": "code", + "execution_count": 7, "id": "e6b97a13", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:34.916076Z", + "iopub.status.busy": "2026-06-07T20:53:34.915910Z", + "iopub.status.idle": "2026-06-07T20:53:35.594571Z", + "shell.execute_reply": "2026-06-07T20:53:35.593660Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test dataset | has_contexts: False\n" + ] + } + ], "source": [ "# Search for KItems inside the project context\n", "results = dsms.search(contexts=[str(project.id)])\n", "for r in results:\n", " print(r.kitem.name, \"| has_contexts:\", r.kitem.has_contexts)" - ], - "metadata": {}, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", "id": "1c020217", - "source": "The `has_contexts` field on `KItemCompactedModel` is a boolean that indicates whether the KItem belongs to at least one context. It is populated by the server and available on search results without needing to fetch the full KItem.", - "metadata": {} + "metadata": {}, + "source": [ + "The `has_contexts` field on `KItemCompactedModel` is a boolean that indicates whether the KItem belongs to at least one context. It is populated by the server and available on search results without needing to fetch the full KItem." + ] }, { "cell_type": "markdown", "id": "32f71fd1", - "source": "## 8.4. Context-scoped SPARQL queries\n\nTwo SPARQL methods on `SparqlInterface` operate within the scope of a context KItem rather than the full triplestore.\n\n`sparql_interface.query_context(context_id, query)` sends a standard SPARQL SELECT query scoped to the given context and returns a JSON result object (same format as `sparql_interface.query()`).\n\n`sparql_interface.graph_context(context_id, query)` sends a graph query scoped to the context and returns a JSON graph result.", - "metadata": {} + "metadata": {}, + "source": [ + "## 8.4. Context-scoped SPARQL queries\n", + "\n", + "Two SPARQL methods on `SparqlInterface` operate within the scope of a context KItem rather than the full triplestore.\n", + "\n", + "`sparql_interface.query_context(context_id, query)` sends a standard SPARQL SELECT query scoped to the given context and returns a JSON result object (same format as `sparql_interface.query()`).\n", + "\n", + "`sparql_interface.graph_context(context_id, query)` sends a graph query scoped to the context and returns a JSON graph result." + ] }, { "cell_type": "code", + "execution_count": 8, "id": "875b69d5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:35.596751Z", + "iopub.status.busy": "2026-06-07T20:53:35.596591Z", + "iopub.status.idle": "2026-06-07T20:53:35.678070Z", + "shell.execute_reply": "2026-06-07T20:53:35.677223Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: context SPARQL requires RDF knowledge graphs for context members.\n", + "Error: Context SPARQL query was not successful: {\"detail\":\"No KG.kitem.ttl attachments found for context members. Ensure knowledge graphs have been generated for the members.\"}\n" + ] + } + ], "source": [ "sparql_query = \"\"\"\n", "SELECT ?s ?p ?o\n", @@ -198,28 +369,36 @@ "except RuntimeError as e:\n", " print(f\"Note: context SPARQL requires RDF knowledge graphs for context members.\")\n", " print(f\"Error: {e}\")" - ], - "metadata": {}, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", "id": "6d37b2bf", - "source": "## 8.5. Cleanup\n\nDelete the KItems created during this tutorial.", - "metadata": {} + "metadata": {}, + "source": [ + "## 8.5. Cleanup\n", + "\n", + "Delete the KItems created during this tutorial." + ] }, { "cell_type": "code", + "execution_count": 9, "id": "6f764bfd", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-07T20:53:35.680027Z", + "iopub.status.busy": "2026-06-07T20:53:35.679870Z", + "iopub.status.idle": "2026-06-07T20:53:36.536942Z", + "shell.execute_reply": "2026-06-07T20:53:36.535707Z" + } + }, + "outputs": [], "source": [ "del dsms[dataset]\n", "del dsms[project]\n", "dsms.commit()" - ], - "metadata": {}, - "execution_count": null, - "outputs": [] + ] } ], "metadata": { @@ -238,7 +417,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index a13db52..5f30961 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ docs = jupyter==1.0.0 myst-parser==4.0.0 nbsphinx==0.9.5 + pytest-nbmake sphinx-autobuild==2024.4.16 sphinx-book-theme==1.1.3 sphinx-copybutton==0.5.2 @@ -60,7 +61,6 @@ pre_commit = tests = pytest>=7.4.3 pytest-mock - pytest-nbmake responses [bumpver] From d34da3a793902732f8ade13ab78de7dba3b7a75a Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Sun, 7 Jun 2026 17:35:41 -0400 Subject: [PATCH 38/48] Fix visibility not sent as top-level field in KItem update diff The backend update endpoint accepts visibility as a top-level field, separate from access_properties. Previously utils._get_kitems_diffs embedded visibility inside the access_properties dict, causing all SDK-driven visibility changes to be silently ignored by the server. Also documents the visibility field in KItemAccessProperties schema docs, fixes the role range from 1-4 to 1-3 (ADMIN removed), and adds two unit tests covering the promoted-visibility and unchanged-visibility cases. --- docs/dsms_sdk/dsms_kitem_schema.md | 39 +++++++++--- dsms/knowledge/utils.py | 9 ++- tests/test_utils.py | 96 ++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/docs/dsms_sdk/dsms_kitem_schema.md b/docs/dsms_sdk/dsms_kitem_schema.md index ac97b2c..7acf6c4 100644 --- a/docs/dsms_sdk/dsms_kitem_schema.md +++ b/docs/dsms_sdk/dsms_kitem_schema.md @@ -292,7 +292,19 @@ sample_kitem.user_groups = [ ## KItemAccessProperties Fields -`KItemAccessProperties` controls who can read, update, delete and manage a KItem. It contains two lists: one for individual users and one for groups. +`KItemAccessProperties` controls who can access a KItem. It has three orthogonal mechanisms: + +- **Visibility** — a single field that grants read access to broad audiences without requiring explicit role assignments. +- **User access** — per-user role assignments for fine-grained control. +- **Group access** — per-Keycloak-group role assignments. + +### Visibility Values + +| Value | Who can read | +|:-----------:|:-------------------------------------------------:| +| `private` | Only users and groups listed in access properties | +| `internal` | All authenticated users | +| `public` | Everyone (no login required) | ### Role Values @@ -301,35 +313,44 @@ sample_kitem.user_groups = [ | `MEMBER` | 1 | READ | | `CONTRIBUTOR` | 2 | READ, UPDATE | | `OWNER` | 3 | READ, UPDATE, DELETE, MANAGE | + ### KItemAccessProperties Sub-fields -| Field Name | Description | Type | Default | Property Namespace | Required/Optional | -|:-------------:|:----------------------------------------:|:--------------------------------:|:-------:|:------------------:|:-----------------:| -| User Access | Per-user role assignments | List[[UserAccessProperty](#useraccessproperty-fields)] | `[]` | `user_access` | Optional | -| Group Access | Per-group role assignments | List[[GroupAccessProperty](#groupaccessproperty-fields)] | `[]` | `group_access` | Optional | +| Field Name | Description | Type | Default | Property Namespace | Required/Optional | +|:-------------:|:-------------------------------------------------:|:--------------------------------:|:-----------:|:------------------:|:-----------------:| +| Visibility | Broad read-access level | `"private"` \| `"internal"` \| `"public"` | `"private"` | `visibility` | Optional | +| User Access | Per-user role assignments | List[[UserAccessProperty](#useraccessproperty-fields)] | `[]` | `user_access` | Optional | +| Group Access | Per-group role assignments (special visibility groups excluded) | List[[GroupAccessProperty](#groupaccessproperty-fields)] | `[]` | `group_access` | Optional | ### UserAccessProperty Fields | Field Name | Description | Type | Default | Property Namespace | Required/Optional | |:----------:|:-----------------------:|:------:|:--------------:|:------------------:|:-----------------:| | User ID | UUID of the user | string | Not Applicable | `user_id` | Required | -| Role | Role assigned to user | int (1–4) or Role name | Not Applicable | `role` | Required | +| Role | Role assigned to user | int (1–3) or Role name | Not Applicable | `role` | Required | ### GroupAccessProperty Fields | Field Name | Description | Type | Default | Property Namespace | Required/Optional | |:----------:|:-----------------------:|:------:|:--------------:|:------------------:|:-----------------:| | Group ID | UUID of the group | string | Not Applicable | `group_id` | Required | -| Role | Role assigned to group | int (1–4) or Role name | Not Applicable | `role` | Required | +| Role | Role assigned to group | int (1–3) or Role name | Not Applicable | `role` | Required | ### Example Usage ```python from dsms.knowledge.properties.access import KItemAccessProperties, Role -# Assign a user as OWNER and a group as MEMBER +# Make an item readable by all authenticated users, with one explicit owner +item.access_properties = KItemAccessProperties( + visibility="internal", + user_access=[{"user_id": "abc-123", "role": Role.OWNER}], +) + +# Grant a specific group contributor access on a private item item.access_properties = KItemAccessProperties( + visibility="private", user_access=[{"user_id": "abc-123", "role": Role.OWNER}], - group_access=[{"group_id": "g-456", "role": Role.MEMBER}], + group_access=[{"group_id": "g-456", "role": Role.CONTRIBUTOR}], ) # Query which users can perform a given operation diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 8ca1ec9..c754b9e 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -614,7 +614,9 @@ def _get_kitems_diffs(kitem_old: "Dict[str, Any]", kitem_new: "KItem"): context_kitems = _get_kitem_contexts(kitem_old, kitem_new) # same holds for kitem apps apps = _get_apps_diff(kitem_old, kitem_new) - # access_properties: include as full replacement when changed + # access_properties: include as full replacement when changed. + # visibility is a top-level field on the backend update model, so it must + # be sent separately rather than nested inside access_properties. old_access = kitem_old.get("access_properties") new_access = ( kitem_new.access_properties.model_dump(mode="json") @@ -622,6 +624,11 @@ def _get_kitems_diffs(kitem_old: "Dict[str, Any]", kitem_new: "KItem"): else None ) if new_access != old_access and new_access is not None: + visibility = new_access.pop("visibility", None) + if visibility is not None: + old_visibility = (old_access or {}).get("visibility") + if visibility != old_visibility: + differences["visibility"] = visibility differences["access_properties"] = new_access # merge with previously found differences differences.update(**linked_kitems, **apps, **context_kitems) diff --git a/tests/test_utils.py b/tests/test_utils.py index 233caf4..9981339 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -178,6 +178,102 @@ def test_kitem_diffs(get_mock_kitem_ids, custom_address): assert len(expected) == 0 +@responses.activate +def test_kitem_diffs_access_properties(get_mock_kitem_ids, custom_address): + """visibility must be promoted to a top-level key; must not appear inside access_properties.""" + import pytest + + from dsms.core.dsms import DSMS + from dsms.knowledge.kitem import KItem + from dsms.knowledge.properties.access import KItemAccessProperties, Role + from dsms.knowledge.utils import _get_kitems_diffs + + with pytest.warns(UserWarning, match="No authentication details"): + dsms = DSMS(host_url=custom_address) + + user_id = "abc-123" + + kitem_old = { + "id": get_mock_kitem_ids[0], + "name": "foo", + "ktype_id": dsms.ktypes.Organization.value, + "annotations": [], + "linked_kitems": [], + "apps": [], + "access_properties": { + "visibility": "private", + "user_access": [{"user_id": user_id, "role": "owner"}], + "group_access": [], + }, + } + + kitem_new = KItem( + id=get_mock_kitem_ids[0], + name="foo-item", + slug="foo-item", + ktype_id=dsms.ktypes.Organization, + access_properties=KItemAccessProperties( + visibility="internal", + user_access=[{"user_id": user_id, "role": Role.OWNER}], + ), + ) + + diffs = _get_kitems_diffs(kitem_old, kitem_new) + + assert diffs["visibility"] == "internal" + assert "visibility" not in diffs["access_properties"] + + +@responses.activate +def test_kitem_diffs_visibility_unchanged(get_mock_kitem_ids, custom_address): + """When visibility hasn't changed it should not appear in the diff.""" + import pytest + + from dsms.core.dsms import DSMS + from dsms.knowledge.kitem import KItem + from dsms.knowledge.properties.access import KItemAccessProperties, Role + from dsms.knowledge.utils import _get_kitems_diffs + + with pytest.warns(UserWarning, match="No authentication details"): + dsms = DSMS(host_url=custom_address) + + user_id = "abc-123" + other_user_id = "def-456" + + kitem_old = { + "id": get_mock_kitem_ids[0], + "name": "foo", + "ktype_id": dsms.ktypes.Organization.value, + "annotations": [], + "linked_kitems": [], + "apps": [], + "access_properties": { + "visibility": "internal", + "user_access": [{"user_id": user_id, "role": "owner"}], + "group_access": [], + }, + } + + kitem_new = KItem( + id=get_mock_kitem_ids[0], + name="foo-item", + slug="foo-item", + ktype_id=dsms.ktypes.Organization, + access_properties=KItemAccessProperties( + visibility="internal", + user_access=[ + {"user_id": user_id, "role": Role.OWNER}, + {"user_id": other_user_id, "role": Role.MEMBER}, + ], + ), + ) + + diffs = _get_kitems_diffs(kitem_old, kitem_new) + + assert "visibility" not in diffs + assert "visibility" not in diffs["access_properties"] + + @responses.activate def test_unit_conversion(custom_address): """Test unit conversion test""" From 36caf62d13beaa3ba1df2a9dae69bee7c9cb5273 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Mon, 8 Jun 2026 05:04:02 -0400 Subject: [PATCH 39/48] Add group CRUD methods, clean up group models - DSMS: add create_group, update_group, delete_group, get_group_members, add_group_member, remove_group_member with cache invalidation - utils.py: add corresponding HTTP helpers for all six operations - groups/models.py: remove GroupListBase (empty list subclass); flat now returns List[BaseGroup] directly; clean up docstrings - groups/__init__.py: remove GroupListBase from exports - properties/__init__.py: remove dead UserGroup import and export - kitem.py: update docstring to reference access_properties instead of the removed user_groups/UserGroup field - tests/test_groups.py: 14 new tests covering all CRUD methods and the GroupListBase removal --- dsms/core/dsms.py | 88 +++++++ dsms/knowledge/groups/__init__.py | 3 +- dsms/knowledge/groups/models.py | 41 ++- dsms/knowledge/kitem.py | 4 +- dsms/knowledge/properties/__init__.py | 2 - dsms/knowledge/utils.py | 100 +++++++ tests/test_groups.py | 364 ++++++++++++++++++++++++++ 7 files changed, 570 insertions(+), 32 deletions(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index c68b5d1..0865b7d 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -30,7 +30,11 @@ from dsms.knowledge.utils import _search from dsms.knowledge.utils import ( # isort:skip + _add_group_member, _commit, + _create_group, + _delete_group, + _get_group_members, _get_kitem, _get_kitem_list, _get_ktypes_by_parent, @@ -40,6 +44,8 @@ _get_schema_data, _get_user_groups, _get_user_list, + _remove_group_member, + _update_group, _v2_create_ktype, _v2_delete_ktype, _v2_export_ktype, @@ -419,6 +425,88 @@ def get_user(self, user_id: str) -> "User": """ return get_user_by_id(self, user_id) + def get_group_members(self, group_id: str) -> "List[User]": + """Fetch the members of a group (including subgroup members). + + Args: + group_id: The unique identifier of the group. + + Returns: + List of User objects belonging to the group. + """ + return _get_group_members(self, group_id) + + def create_group( + self, + name: str, + description: str = "", + parent_id: Optional[str] = None, + ) -> "Group": + """Create a new group. + + Args: + name: Name of the group. + description: Optional description. + parent_id: ID of the parent group. If None, creates a top-level group. + + Returns: + The created Group object. + """ + from dsms.knowledge.groups import Group # noqa: F401 + + group = _create_group(self, name, description, parent_id) + self._user_groups = None # invalidate cache + return group + + def update_group( + self, + group_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> "Group": + """Update the name or description of an existing group. + + Args: + group_id: The unique identifier of the group. + name: New name. Pass None to leave unchanged. + description: New description. Pass None to leave unchanged. + + Returns: + The updated Group object. + """ + from dsms.knowledge.groups import Group # noqa: F401 + + group = _update_group(self, group_id, name, description) + self._user_groups = None # invalidate cache + return group + + def delete_group(self, group_id: str) -> None: + """Delete a group. + + Args: + group_id: The unique identifier of the group to delete. + """ + _delete_group(self, group_id) + self._user_groups = None # invalidate cache + + def add_group_member(self, group_id: str, user_id: str) -> None: + """Add a user to a group. + + Args: + group_id: The unique identifier of the group. + user_id: The unique identifier of the user to add. + """ + _add_group_member(self, group_id, user_id) + + def remove_group_member(self, group_id: str, user_id: str) -> None: + """Remove a user from a group. + + Args: + group_id: The unique identifier of the group. + user_id: The unique identifier of the user to remove. + """ + _remove_group_member(self, group_id, user_id) + def get_schema_data(self, kitem_id: str) -> "List[KItemSchemaData]": """Fetch all schema-data entries for a KItem from the remote backend. diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index 204b1af..6c767a6 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -1,6 +1,6 @@ """DSMS User Groups Module.""" -from .models import BaseGroup, Group, GroupList, GroupListBase, User, UserList +from .models import BaseGroup, Group, GroupList, User, UserList from .public import ( INTERNAL_GROUP, PUBLIC_GROUP, @@ -10,7 +10,6 @@ __all__ = [ "Group", "GroupList", - "GroupListBase", "INTERNAL_GROUP", "PUBLIC_GROUP", "refresh_public_groups", diff --git a/dsms/knowledge/groups/models.py b/dsms/knowledge/groups/models.py index 642c47f..d3b339d 100644 --- a/dsms/knowledge/groups/models.py +++ b/dsms/knowledge/groups/models.py @@ -25,11 +25,9 @@ class User(BaseModel): ) def __repr__(self) -> str: - """String representation of the GroupList.""" return str(self) def __str__(self): - """Pretty print the User""" from dsms.knowledge.utils import print_model return print_model( @@ -43,11 +41,9 @@ class UserList(list): """List of Users with utility methods.""" def __repr__(self) -> str: - """String representation of the GroupList.""" return str(self) def __str__(self): - """Pretty print the UserList""" from dsms.knowledge.utils import dump_model return yaml.dump( @@ -72,78 +68,71 @@ def by_username(self) -> dict[str, User]: @property def by_name(self) -> dict[str, User]: - """Return a dictionary of users indexed by their username.""" + """Alias for by_username.""" return self.by_username def __getitem__(self, user_id: str) -> User: - """Get a user by ID""" - + """Get a user by ID.""" return self.by_id[user_id] class BaseGroup(BaseModel): - """User Group Model""" + """Flat group model — id and name only, no subgroups.""" id: str = Field(..., description="The unique identifier of the group.") name: str = Field(..., description="The name of the group.") class Group(BaseGroup): - """User Group Model with Subgroups""" + """Group model with optional subgroup hierarchy.""" subgroups: Optional[List["Group"]] = Field( None, description="A list of subgroups." ) -class GroupListBase(list): - """Base class for GroupList with utility methods.""" +class GroupList(list): + """List of Groups (may be hierarchical) with utility methods.""" def __repr__(self) -> str: - """String representation of the GroupList.""" return str(self) def __str__(self): - """Pretty print the GroupList""" from dsms.knowledge.utils import dump_model return yaml.dump( [ dump_model( - connection, + g, exclude_extra=Session.dsms.config.hide_properties, ) - for connection in self + for g in self ] ) - -class GroupList(list): - """List of Groups with utility methods.""" - @property - def flat(self) -> List[Group]: - """Return a flat list of all groups and their subgroups.""" - flat_list = [] + def flat(self) -> List[BaseGroup]: + """Return a flat list of BaseGroup objects for all groups and subgroups.""" + result: List[BaseGroup] = [] def _flatten(groups: List[Group]): for group in groups: - flat_list.append( + result.append( BaseGroup(**group.model_dump(exclude={"subgroups"})) ) if group.subgroups: _flatten(group.subgroups) _flatten(self) - return GroupListBase(flat_list) + return result @property - def by_id(self) -> dict[str, Group]: + def by_id(self) -> dict[str, BaseGroup]: """Return a dictionary of groups indexed by their ID.""" return {group.id: group for group in self.flat} @property - def by_name(self) -> dict[str, Group]: + def by_name(self) -> dict[str, BaseGroup]: """Return a dictionary of groups indexed by their name.""" return {group.name: group for group in self.flat} diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index a23e991..3bbd412 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -117,8 +117,8 @@ class KItem(KItemCompactedModel): apps (List[App]): Apps related to the KItem. summary (Optional[Union[str, Summary]]): Human readable summary text of the KItem. - user_groups (List[UserGroup]): - User groups able to access the KItem. + access_properties (Optional[KItemAccessProperties]): + Access control configuration for the KItem. custom_properties (Optional[Any]): Custom properties associated with the KItem. dataframe (Optional[Union[List[Column], pd.DataFrame, Dict[str, Union[List, Dict]]]]): diff --git a/dsms/knowledge/properties/__init__.py b/dsms/knowledge/properties/__init__.py index 261e6dc..03a152c 100644 --- a/dsms/knowledge/properties/__init__.py +++ b/dsms/knowledge/properties/__init__.py @@ -13,7 +13,6 @@ from dsms.knowledge.properties.contacts import ContactInfo from dsms.knowledge.properties.dataframe import Column, DataFrameContainer from dsms.knowledge.properties.summary import Summary -from dsms.knowledge.properties.user_groups import UserGroup from dsms.knowledge.properties.attachments import ( # isort:skip Attachment, @@ -49,7 +48,6 @@ "ContactInfo", "ExternalLink", "Affiliation", - "UserGroup", "Summary", "DataFrameContainer", "Column", diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index c754b9e..f26c3ca 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -331,6 +331,10 @@ def _create_new_kitem(kitem: "KItem") -> "Dict[str, Any]": "slug": kitem.slug, "ktype_id": kitem.ktype.id, } + if kitem.access_properties is not None: + access = kitem.access_properties.model_dump(mode="json") + payload["visibility"] = access.pop("visibility", "private") + payload["access_properties"] = access logger.debug("Create new KItem with payload: %s", payload) response = _perform_request( kitem.dsms, "api/knowledge/kitems", "post", json=payload @@ -1657,6 +1661,102 @@ def _get_user_groups(dsms: "DSMS"): return GroupList([Group(**group) for group in groups]) +def _get_group_members(dsms: "DSMS", group_id: str) -> List[Dict[str, Any]]: + """Fetch members of a specific group from the DSMS backend.""" + from dsms.knowledge.groups import User + + response = _perform_request( + dsms, + f"api/users/groups/{group_id}/members", + "get", + ) + if not response.ok: + raise ConnectionError( + f"Failed to fetch members of group {group_id}: {response.text}" + ) + return [User(**u) for u in response.json()] + + +def _create_group( + dsms: "DSMS", + name: str, + description: str = "", + parent_id: Optional[str] = None, +) -> Dict[str, Any]: + """Create a new group (top-level or subgroup) in the DSMS backend.""" + from dsms.knowledge.groups import Group + + if parent_id: + url = f"api/users/groups/{parent_id}/subgroups" + else: + url = "api/users/groups" + payload = {"name": name, "description": description} + response = _perform_request(dsms, url, "post", json=payload) + if not response.ok: + raise ValueError(f"Failed to create group '{name}': {response.text}") + return Group(**response.json()) + + +def _update_group( + dsms: "DSMS", + group_id: str, + name: Optional[str] = None, + description: Optional[str] = None, +) -> Dict[str, Any]: + """Update name or description of an existing group.""" + from dsms.knowledge.groups import Group + + payload = {} + if name is not None: + payload["name"] = name + if description is not None: + payload["description"] = description + if not payload: + raise ValueError( + "At least one of name or description must be provided." + ) + response = _perform_request( + dsms, f"api/users/groups/{group_id}", "put", json=payload + ) + if not response.ok: + raise ValueError(f"Failed to update group {group_id}: {response.text}") + return Group(**response.json()) + + +def _delete_group(dsms: "DSMS", group_id: str) -> None: + """Delete a group from the DSMS backend.""" + response = _perform_request(dsms, f"api/users/groups/{group_id}", "delete") + if not response.ok: + raise ValueError(f"Failed to delete group {group_id}: {response.text}") + + +def _add_group_member(dsms: "DSMS", group_id: str, user_id: str) -> None: + """Add a user to a group.""" + response = _perform_request( + dsms, + f"api/users/groups/{group_id}/members", + "post", + json={"user_id": user_id}, + ) + if not response.ok: + raise ValueError( + f"Failed to add user {user_id} to group {group_id}: {response.text}" + ) + + +def _remove_group_member(dsms: "DSMS", group_id: str, user_id: str) -> None: + """Remove a user from a group.""" + response = _perform_request( + dsms, + f"api/users/groups/{group_id}/members/{user_id}", + "delete", + ) + if not response.ok: + raise ValueError( + f"Failed to remove user {user_id} from group {group_id}: {response.text}" + ) + + def _get_user_list(dsms: "DSMS"): """Fetch all users from the DSMS backend.""" from dsms.knowledge.groups import User, UserList diff --git a/tests/test_groups.py b/tests/test_groups.py index 737cc70..3ff0b9b 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -425,3 +425,367 @@ def test_get_user_raises_on_missing(custom_address): with pytest.raises(ValueError, match="nonexistent"): dsms.get_user("nonexistent") + + +# --------------------------------------------------------------------------- +# Group CRUD — get_group_members +# --------------------------------------------------------------------------- + +MOCK_MEMBERS = [ + { + "id": "u-1", + "username": "alice", + "firstName": "Alice", + "lastName": "A", + "email": "alice@example.com", + }, + { + "id": "u-2", + "username": "bob", + "firstName": "Bob", + "lastName": "B", + "email": "bob@example.com", + }, +] + + +@responses_lib.activate +def test_get_group_members_returns_user_list(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups/grp-1/members"), + json=MOCK_MEMBERS, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + members = dsms.get_group_members("grp-1") + assert len(members) == 2 + assert all(isinstance(m, User) for m in members) + assert members[0].username == "alice" + assert members[1].username == "bob" + + +@responses_lib.activate +def test_get_group_members_raises_on_error(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups/bad-id/members"), + json={"detail": "Not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ConnectionError, match="bad-id"): + dsms.get_group_members("bad-id") + + +# --------------------------------------------------------------------------- +# Group CRUD — create_group +# --------------------------------------------------------------------------- + +MOCK_NEW_GROUP = {"id": "grp-new", "name": "New Group"} +MOCK_NEW_SUBGROUP = {"id": "grp-sub", "name": "Sub Group"} + + +@responses_lib.activate +def test_create_group_top_level(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin(custom_address, "api/users/groups"), + json=MOCK_NEW_GROUP, + status=201, + ) + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups"), + json=[MOCK_NEW_GROUP], + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + group = dsms.create_group("New Group") + assert isinstance(group, Group) + assert group.id == "grp-new" + assert group.name == "New Group" + # cache must be invalidated + assert dsms._user_groups is None + + +@responses_lib.activate +def test_create_group_as_subgroup(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin(custom_address, "api/users/groups/grp-1/subgroups"), + json=MOCK_NEW_SUBGROUP, + status=201, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + group = dsms.create_group("Sub Group", parent_id="grp-1") + assert isinstance(group, Group) + assert group.id == "grp-sub" + + call = responses_lib.calls[0] + assert "grp-1/subgroups" in call.request.url + + +@responses_lib.activate +def test_create_group_raises_on_error(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin(custom_address, "api/users/groups"), + json={"detail": "Bad request"}, + status=400, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="Failed to create group"): + dsms.create_group("Bad") + + +# --------------------------------------------------------------------------- +# Group CRUD — update_group +# --------------------------------------------------------------------------- + + +@responses_lib.activate +def test_update_group_name(custom_address): + responses_lib.add( + responses_lib.PUT, + urljoin(custom_address, "api/users/groups/grp-1"), + json={"id": "grp-1", "name": "Renamed"}, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + updated = dsms.update_group("grp-1", name="Renamed") + assert isinstance(updated, Group) + assert updated.name == "Renamed" + assert dsms._user_groups is None + + +@responses_lib.activate +def test_update_group_raises_when_nothing_provided(custom_address): + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="At least one"): + dsms.update_group("grp-1") + + +# --------------------------------------------------------------------------- +# Group CRUD — delete_group +# --------------------------------------------------------------------------- + + +@responses_lib.activate +def test_delete_group(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin(custom_address, "api/users/groups/grp-1"), + status=204, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + dsms.delete_group("grp-1") + assert dsms._user_groups is None + assert len(responses_lib.calls) == 1 + assert "groups/grp-1" in responses_lib.calls[0].request.url + + +@responses_lib.activate +def test_delete_group_raises_on_error(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin(custom_address, "api/users/groups/grp-missing"), + json={"detail": "Not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="Failed to delete"): + dsms.delete_group("grp-missing") + + +# --------------------------------------------------------------------------- +# Group CRUD — add_group_member / remove_group_member +# --------------------------------------------------------------------------- + + +@responses_lib.activate +def test_add_group_member(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin(custom_address, "api/users/groups/grp-1/members"), + status=204, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + dsms.add_group_member("grp-1", "u-1") + call = responses_lib.calls[0] + assert "groups/grp-1/members" in call.request.url + import json as _json + + body = _json.loads(call.request.body) + assert body["user_id"] == "u-1" + + +@responses_lib.activate +def test_add_group_member_raises_on_error(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin(custom_address, "api/users/groups/grp-1/members"), + json={"detail": "User not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="Failed to add user"): + dsms.add_group_member("grp-1", "bad-user") + + +@responses_lib.activate +def test_remove_group_member(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin(custom_address, "api/users/groups/grp-1/members/u-1"), + status=204, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + dsms.remove_group_member("grp-1", "u-1") + call = responses_lib.calls[0] + assert "groups/grp-1/members/u-1" in call.request.url + + +@responses_lib.activate +def test_remove_group_member_raises_on_error(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin(custom_address, "api/users/groups/grp-1/members/u-bad"), + json={"detail": "User not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="Failed to remove user"): + dsms.remove_group_member("grp-1", "u-bad") + + +# --------------------------------------------------------------------------- +# GroupListBase removed — flat returns a plain list of BaseGroup +# --------------------------------------------------------------------------- + + +def test_grouplist_flat_is_plain_list(): + """After removing GroupListBase, .flat must return a plain list.""" + gl = GroupList( + [Group(id="a", name="A", subgroups=[Group(id="b", name="B")])] + ) + result = gl.flat + assert isinstance(result, list) + assert not type(result).__name__ == "GroupListBase" + assert all(isinstance(g, BaseGroup) for g in result) From 2ccbd8d66cc43fadda6b6b2197ac311e5f9bc5d1 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Mon, 8 Jun 2026 17:19:02 -0400 Subject: [PATCH 40/48] Replace split user/group access model with unified AccessGrant Removes BaseAccessProperty, UserAccessProperty, GroupAccessProperty and the user_access/group_access fields. Replaces them with a single grants: List[AccessGrant] where each grant carries id, type (user|group), and role. Duplicate detection now keyed on (id, type) pairs. Properties by_user/by_group/user_by_role/group_by_role replaced by by_id, by_role, and operation_by_principal. Updates both test files accordingly. --- dsms/knowledge/properties/access.py | 140 ++++------- tests/test_access.py | 371 ++++++++++++---------------- tests/test_access_extended.py | 94 +++---- 3 files changed, 252 insertions(+), 353 deletions(-) diff --git a/dsms/knowledge/properties/access.py b/dsms/knowledge/properties/access.py index ca09180..4cd1e1f 100644 --- a/dsms/knowledge/properties/access.py +++ b/dsms/knowledge/properties/access.py @@ -1,7 +1,7 @@ """KItem Access Property Module""" from enum import Enum, auto -from typing import Dict, List, Literal, Optional +from typing import Dict, List, Literal from pydantic import BaseModel, Field, field_serializer, field_validator @@ -91,19 +91,26 @@ def max_access_level(cls, operation: OperationType) -> Role: return Role(max(candidates)) -class BaseAccessProperty(BaseModel): - """KItem Access Property Model""" +class AccessGrant(BaseModel): + """A single principal (user or group) grant on a KItem.""" + id: str = Field( + ..., + description="The unique identifier of the user or group.", + ) + type: Literal["user", "group"] = Field( + ..., + description="Whether this grant is for a user or a group.", + ) role: Role = Field( ..., description="Defines the role mapping for access control.", - example=RoleMapping.OWNER, ) @field_validator("role", mode="before") @classmethod def parse_role(cls, v): - """Accept string names (e.g. 'owner') or legacy integer values.""" + """Accept string names (e.g. 'owner') or integer values.""" if isinstance(v, str): return Role[v.upper()] if isinstance(v, int): @@ -112,37 +119,15 @@ def parse_role(cls, v): @property def access_level(self) -> List[OperationType]: - """Set access level based on role""" + """Operations granted by this role.""" return RoleMapping.get_operations(self.role) @field_serializer("role") def serialize_role_json(self, value: Role, _info): - """Serialize role to JSON""" + """Serialize role as uppercase string in Python mode, lowercase for JSON.""" if _info.mode == "python": - return value.name # Python mode: uppercase name - return ( - value.name.lower() - ) # wire mode: "member", "contributor", "owner" - - -class UserAccessProperty(BaseAccessProperty): - """KItem User Access Property Model""" - - user_id: str = Field( - ..., - description="The unique identifier of the user.", - example="1a3b5c7d-9e0f-4g2h-8i1j-2k3l4m5n6o7p", - ) - - -class GroupAccessProperty(BaseAccessProperty): - """KItem Group Access Property Model""" - - group_id: str = Field( - ..., - description="The unique identifier of the group.", - example="g1h2i3j4-k5l6-m7n8-o9p0-q1r2s3t4u5v6", - ) + return value.name + return value.name.lower() class KItemAccessProperties(BaseModel): @@ -155,13 +140,9 @@ class KItemAccessProperties(BaseModel): "internal (all authenticated users), public (everyone)." ), ) - user_access: Optional[List[UserAccessProperty]] = Field( - [], - description="List of user access properties.", - ) - group_access: Optional[List[GroupAccessProperty]] = Field( - [], - description="List of group access properties (special visibility groups are excluded).", + grants: List[AccessGrant] = Field( + default_factory=list, + description="Unified list of user and group access grants.", ) def __str__(self) -> str: @@ -175,74 +156,43 @@ def __str__(self) -> str: def __repr__(self) -> str: return str(self) - @field_validator("user_access", "group_access", mode="after") + @field_validator("grants", mode="after") @classmethod def check_duplicates(cls, v): - """Ensure no duplicate user or group IDs""" + """Ensure no duplicate principal (id, type) pairs.""" if v is None: return [] - seen = set() + seen: set = set() for item in v: - identifier = ( - item.user_id - if isinstance(item, UserAccessProperty) - else item.group_id - ) - if identifier in seen: - raise ValueError(f"Duplicate identifier found: {identifier}") - seen.add(identifier) + key = (item.id, item.type) + if key in seen: + raise ValueError(f"Duplicate grant found: {key}") + seen.add(key) return v @property - def by_user(self) -> Dict[str, UserAccessProperty]: - """Get user access properties""" - return {uap.user_id: uap for uap in self.user_access} - - @property - def by_group(self) -> Dict[str, GroupAccessProperty]: - """Get group access properties""" - return {gap.group_id: gap for gap in self.group_access} + def by_id(self) -> Dict[str, "AccessGrant"]: + """Get grants indexed by principal ID.""" + return {g.id: g for g in self.grants} @property - def operation_by_user(self) -> Dict[OperationType, List[str]]: - """Get access properties by operation type""" - operation_dict: Dict[OperationType, List[str]] = {} - for uap in self.user_access: - for operation in uap.access_level: - if operation not in operation_dict: - operation_dict[operation] = [] - if uap.user_id not in operation_dict[operation]: - operation_dict[operation].append(uap.user_id) - return operation_dict - - @property - def operation_by_group(self) -> Dict[OperationType, List[str]]: - """Get group access properties by operation type""" - operation_dict: Dict[OperationType, List[str]] = {} - for gap in self.group_access: - for operation in gap.access_level: - if operation not in operation_dict: - operation_dict[operation] = [] - if gap.group_id not in operation_dict[operation]: - operation_dict[operation].append(gap.group_id) - return operation_dict - - @property - def user_by_role(self) -> Dict[Role, List[str]]: - """Get users by role""" + def by_role(self) -> Dict[Role, List[str]]: + """Get principal IDs grouped by role.""" role_dict: Dict[Role, List[str]] = {} - for uap in self.user_access: - if uap.role not in role_dict: - role_dict[uap.role] = [] - role_dict[uap.role].append(uap.user_id) + for g in self.grants: + if g.role not in role_dict: + role_dict[g.role] = [] + role_dict[g.role].append(g.id) return role_dict @property - def group_by_role(self) -> Dict[Role, List[str]]: - """Get groups by role""" - role_dict: Dict[Role, List[str]] = {} - for gap in self.group_access: - if gap.role not in role_dict: - role_dict[gap.role] = [] - role_dict[gap.role].append(gap.group_id) - return role_dict + def operation_by_principal(self) -> Dict[OperationType, List[str]]: + """Get principal IDs grouped by operation type.""" + op_dict: Dict[OperationType, List[str]] = {} + for g in self.grants: + for operation in g.access_level: + if operation not in op_dict: + op_dict[operation] = [] + if g.id not in op_dict[operation]: + op_dict[operation].append(g.id) + return op_dict diff --git a/tests/test_access.py b/tests/test_access.py index ddc8d05..bf38813 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -1,4 +1,4 @@ -""" "Tests for Access Property Module""" +"""Tests for Access Property Module""" from typing import List @@ -6,57 +6,51 @@ from pydantic import ValidationError from dsms.knowledge.properties.access import ( - BaseAccessProperty, - GroupAccessProperty, + AccessGrant, KItemAccessProperties, OperationType, Role, RoleMapping, - UserAccessProperty, ) @pytest.fixture -def sample_user_access() -> List[UserAccessProperty]: - """Create sample user access properties""" +def sample_user_grants() -> List[AccessGrant]: return [ - UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user2", role=Role.MEMBER), - UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), + AccessGrant(id="user1", type="user", role=Role.OWNER), + AccessGrant(id="user2", type="user", role=Role.MEMBER), + AccessGrant(id="user3", type="user", role=Role.CONTRIBUTOR), ] @pytest.fixture -def sample_group_access() -> List[GroupAccessProperty]: - """Create sample group access properties""" +def sample_group_grants() -> List[AccessGrant]: return [ - GroupAccessProperty(group_id="group1", role=Role.OWNER), - GroupAccessProperty(group_id="group2", role=Role.MEMBER), + AccessGrant(id="group1", type="group", role=Role.OWNER), + AccessGrant(id="group2", type="group", role=Role.MEMBER), ] @pytest.fixture def access_properties( - sample_user_access, sample_group_access + sample_user_grants, sample_group_grants ) -> KItemAccessProperties: - """Create KItemAccessProperties instance with sample data""" return KItemAccessProperties( - user_access=sample_user_access, - group_access=sample_group_access, + grants=sample_user_grants + sample_group_grants, ) def test_access_level_owner(): """Test access_level property for OWNER role""" - prop = BaseAccessProperty(role=Role.OWNER) + grant = AccessGrant(id="u1", type="user", role=Role.OWNER) expected = [ OperationType.READ, OperationType.UPDATE, OperationType.DELETE, OperationType.MANAGE, ] - assert prop.access_level == expected - assert prop.role.value == Role.OWNER.value + assert grant.access_level == expected + assert grant.role.value == Role.OWNER.value def test_minimum_access_level(): @@ -90,28 +84,26 @@ def test_maximum_access_level(): ) -def test_access_level_user(): - """Test access_level property for USER role""" - prop = BaseAccessProperty(role=Role.MEMBER) +def test_access_level_member(): + """Test access_level property for MEMBER role""" + grant = AccessGrant(id="u1", type="user", role=Role.MEMBER) expected = [OperationType.READ] - assert prop.access_level == expected - assert prop.role.value == Role.MEMBER.value + assert grant.access_level == expected + assert grant.role.value == Role.MEMBER.value def test_access_level_contributor(): """Test access_level property for CONTRIBUTOR role""" - prop = BaseAccessProperty(role=Role.CONTRIBUTOR) + grant = AccessGrant(id="u1", type="user", role=Role.CONTRIBUTOR) expected = [OperationType.READ, OperationType.UPDATE] - assert prop.access_level == expected - assert prop.role.value == Role.CONTRIBUTOR.value + assert grant.access_level == expected + assert grant.role.value == Role.CONTRIBUTOR.value -@pytest.mark.usefixtures("access_properties") -def test_by_user_property(access_properties): - """Test by_user property returns correct user mapping""" - result = access_properties.by_user +def test_by_id_users(access_properties): + """Test by_id property returns correct lookup for user grants""" + result = access_properties.by_id - assert len(result) == 3 assert "user1" in result assert "user2" in result assert "user3" in result @@ -124,12 +116,10 @@ def test_by_user_property(access_properties): assert result["user3"].role.value == Role.CONTRIBUTOR.value -@pytest.mark.usefixtures("access_properties") -def test_by_group_property(access_properties): - """Test by_group property returns correct group mapping""" - result = access_properties.by_group +def test_by_id_groups(access_properties): + """Test by_id property returns correct lookup for group grants""" + result = access_properties.by_id - assert len(result) == 2 assert "group1" in result assert "group2" in result @@ -139,235 +129,190 @@ def test_by_group_property(access_properties): assert result["group2"].role.value == Role.MEMBER.value -@pytest.mark.usefixtures("access_properties") -def test_operation_by_user_property(access_properties): - """Test operation_by_user property returns correct operation mapping""" - result = access_properties.operation_by_user - - # user1 (OWNER): READ, UPDATE, DELETE, MANAGE - # user2 (USER): READ - # user3 (CONTRIBUTOR): READ, UPDATE - - expected_read = ["user1", "user2", "user3"] - expected_update = ["user1", "user3"] - expected_delete = ["user1"] - expected_manage = ["user1"] - - assert set(result[OperationType.READ]) == set(expected_read) - assert set(result[OperationType.UPDATE]) == set(expected_update) - assert set(result[OperationType.DELETE]) == set(expected_delete) - assert set(result[OperationType.MANAGE]) == set(expected_manage) - - -@pytest.mark.usefixtures("access_properties") -def test_operation_by_group_property(access_properties): - """Test operation_by_group property returns correct operation mapping""" - result = access_properties.operation_by_group - - # group1 (OWNER): READ, UPDATE, DELETE, MANAGE - # group2 (MEMBER): READ - - expected_read = ["group1", "group2"] - expected_update = ["group1"] - expected_delete = ["group1"] - expected_manage = ["group1"] - - assert set(result[OperationType.READ]) == set(expected_read) - assert set(result[OperationType.UPDATE]) == set(expected_update) - assert set(result[OperationType.DELETE]) == set(expected_delete) - assert set(result[OperationType.MANAGE]) == set(expected_manage) - +def test_operation_by_user_principal(): + """Test operation_by_principal for user grants""" + props = KItemAccessProperties( + grants=[ + AccessGrant(id="user1", type="user", role=Role.OWNER), + AccessGrant(id="user2", type="user", role=Role.MEMBER), + AccessGrant(id="user3", type="user", role=Role.CONTRIBUTOR), + ] + ) + result = props.operation_by_principal -def test_operation_by_user_multiple_same_operation(): - """Test operation_by_user with multiple users having same operations""" - user_access = [ - UserAccessProperty(user_id="user1", role=Role.MEMBER), - UserAccessProperty(user_id="user2", role=Role.MEMBER), - UserAccessProperty(user_id="user3", role=Role.CONTRIBUTOR), - ] - props = KItemAccessProperties(user_access=user_access) - result = props.operation_by_user + assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} + assert set(result[OperationType.UPDATE]) == {"user1", "user3"} + assert result[OperationType.DELETE] == ["user1"] + assert result[OperationType.MANAGE] == ["user1"] + + +def test_operation_by_group_principal(): + """Test operation_by_principal for group grants""" + props = KItemAccessProperties( + grants=[ + AccessGrant(id="group1", type="group", role=Role.OWNER), + AccessGrant(id="group2", type="group", role=Role.MEMBER), + ] + ) + result = props.operation_by_principal + + assert set(result[OperationType.READ]) == {"group1", "group2"} + assert result[OperationType.UPDATE] == ["group1"] + assert result[OperationType.DELETE] == ["group1"] + assert result[OperationType.MANAGE] == ["group1"] + + +def test_operation_by_principal_multiple_same_operation(): + """Test operation_by_principal with multiple principals having same operations""" + props = KItemAccessProperties( + grants=[ + AccessGrant(id="user1", type="user", role=Role.MEMBER), + AccessGrant(id="user2", type="user", role=Role.MEMBER), + AccessGrant(id="user3", type="user", role=Role.CONTRIBUTOR), + ] + ) + result = props.operation_by_principal - # All users should have READ access assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} - # Only user3 (CONTRIBUTOR) should have UPDATE access assert result[OperationType.UPDATE] == ["user3"] -def test_operation_by_group_from_int(): - """Test operation_by_user with multiple users having same operations""" - user_access = [ - UserAccessProperty(user_id="user1", role=1), - UserAccessProperty(user_id="user2", role=1), - UserAccessProperty(user_id="user3", role=2), - ] - props = KItemAccessProperties(user_access=user_access) - result = props.operation_by_user +def test_role_parsed_from_int(): + """Test that roles parsed from integer values work correctly""" + props = KItemAccessProperties( + grants=[ + AccessGrant(id="user1", type="user", role=1), + AccessGrant(id="user2", type="user", role=1), + AccessGrant(id="user3", type="user", role=2), + ] + ) + result = props.operation_by_principal - # All users should have READ access assert set(result[OperationType.READ]) == {"user1", "user2", "user3"} - # Only user3 (CONTRIBUTOR) should have UPDATE access assert result[OperationType.UPDATE] == ["user3"] - assert props.by_user["user1"].role == Role.MEMBER - assert props.by_user["user1"].role.value == Role.MEMBER.value - - -def test_operation_by_group_multiple_same_operation(): - """Test operation_by_group with multiple groups having same operations""" - group_access = [ - GroupAccessProperty(group_id="group1", role=Role.MEMBER), - GroupAccessProperty(group_id="group2", role=Role.MEMBER), - GroupAccessProperty(group_id="group3", role=Role.OWNER), - ] - props = KItemAccessProperties(group_access=group_access) - result = props.operation_by_group + assert props.by_id["user1"].role == Role.MEMBER + assert props.by_id["user1"].role.value == Role.MEMBER.value + + +def test_by_role_groups(): + """Test by_role with group grants""" + props = KItemAccessProperties( + grants=[ + AccessGrant(id="group1", type="group", role=Role.MEMBER), + AccessGrant(id="group2", type="group", role=Role.MEMBER), + AccessGrant(id="group3", type="group", role=Role.OWNER), + ] + ) + result = props.operation_by_principal - # All groups should have READ access assert set(result[OperationType.READ]) == {"group1", "group2", "group3"} - # Only group3 (OWNER) should have MANAGE access assert result[OperationType.MANAGE] == ["group3"] - assert props.group_by_role[Role.OWNER] == ["group3"] - assert props.group_by_role[Role.MEMBER] == ["group1", "group2"] + assert props.by_role[Role.OWNER] == ["group3"] + assert props.by_role[Role.MEMBER] == ["group1", "group2"] def test_model_creation_with_defaults(): """Test model creation with default values""" props = KItemAccessProperties() - assert props.user_access == [] - assert props.group_access == [] - assert props.by_user == {} - assert props.by_group == {} - assert props.operation_by_user == {} - assert props.operation_by_group == {} + assert props.grants == [] + assert props.visibility == "private" + assert props.by_id == {} + assert props.by_role == {} + assert props.operation_by_principal == {} -def test_user_access_property_creation(): - """Test UserAccessProperty creation and access_level inheritance""" - user_prop = UserAccessProperty(user_id="test_user", role=Role.CONTRIBUTOR) +def test_grant_creation(): + """Test AccessGrant creation and access_level""" + grant = AccessGrant(id="test_user", type="user", role=Role.CONTRIBUTOR) - assert user_prop.user_id == "test_user" - assert user_prop.role == Role.CONTRIBUTOR - assert user_prop.access_level == [OperationType.READ, OperationType.UPDATE] + assert grant.id == "test_user" + assert grant.type == "user" + assert grant.role == Role.CONTRIBUTOR + assert grant.access_level == [OperationType.READ, OperationType.UPDATE] -def test_duplicate_user_ids_raises_error(): - """Test that duplicate user IDs raise ValueError""" - user_access = [ - UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user2", role=Role.MEMBER), - UserAccessProperty( - user_id="user1", role=Role.CONTRIBUTOR +def test_duplicate_user_grants_raises_error(): + """Test that duplicate (id, type) user grants raise ValueError""" + grants = [ + AccessGrant(id="user1", type="user", role=Role.OWNER), + AccessGrant(id="user2", type="user", role=Role.MEMBER), + AccessGrant( + id="user1", type="user", role=Role.CONTRIBUTOR ), # Duplicate ] with pytest.raises(ValidationError) as exc_info: - KItemAccessProperties(user_access=user_access, group_access=[]) + KItemAccessProperties(grants=grants) - # Check that the ValueError with the correct message is included error_details = exc_info.value.errors() assert len(error_details) == 1 assert error_details[0]["type"] == "value_error" - assert "Duplicate identifier found: user1" in str( - error_details[0]["ctx"]["error"] - ) + assert "Duplicate grant found" in str(error_details[0]["ctx"]["error"]) -def test_duplicate_group_ids_raises_error(): - """Test that duplicate group IDs raise ValueError""" - group_access = [ - GroupAccessProperty(group_id="group1", role=Role.OWNER), - GroupAccessProperty(group_id="group2", role=Role.MEMBER), - GroupAccessProperty( - group_id="group1", role=Role.CONTRIBUTOR +def test_duplicate_group_grants_raises_error(): + """Test that duplicate (id, type) group grants raise ValueError""" + grants = [ + AccessGrant(id="group1", type="group", role=Role.OWNER), + AccessGrant(id="group2", type="group", role=Role.MEMBER), + AccessGrant( + id="group1", type="group", role=Role.CONTRIBUTOR ), # Duplicate ] with pytest.raises(ValidationError) as exc_info: - KItemAccessProperties(user_access=[], group_access=group_access) + KItemAccessProperties(grants=grants) error_details = exc_info.value.errors() assert len(error_details) == 1 assert error_details[0]["type"] == "value_error" - assert "Duplicate identifier found: group1" in str( - error_details[0]["ctx"]["error"] - ) + assert "Duplicate grant found" in str(error_details[0]["ctx"]["error"]) -def test_both_user_and_group_duplicates_raises_multiple_errors(): - """Test that duplicates in both user and group access raise multiple errors""" - user_access = [ - UserAccessProperty(user_id="user1", role=Role.OWNER), - UserAccessProperty(user_id="user1", role=Role.MEMBER), # Duplicate +def test_same_id_different_type_no_error(): + """Test that the same ID for user and group is not a duplicate""" + grants = [ + AccessGrant(id="shared-id", type="user", role=Role.OWNER), + AccessGrant(id="shared-id", type="group", role=Role.MEMBER), ] - group_access = [ - GroupAccessProperty(group_id="group1", role=Role.OWNER), - GroupAccessProperty(group_id="group1", role=Role.MEMBER), # Duplicate - ] - - with pytest.raises(ValidationError) as exc_info: - KItemAccessProperties( - user_access=user_access, group_access=group_access - ) - - error_details = exc_info.value.errors() - assert len(error_details) == 2 - - # Check both errors - user_error = next( - err for err in error_details if err["loc"] == ("user_access",) - ) - group_error = next( - err for err in error_details if err["loc"] == ("group_access",) - ) - - assert "Duplicate identifier found: user1" in str( - user_error["ctx"]["error"] - ) - assert "Duplicate identifier found: group1" in str( - group_error["ctx"]["error"] - ) + props = KItemAccessProperties(grants=grants) + assert len(props.grants) == 2 def test_case_sensitive_ids(): """Test that IDs are case sensitive (no duplicates if different case)""" - user_access = [ - UserAccessProperty(user_id="User1", role=Role.OWNER), - UserAccessProperty( - user_id="user1", role=Role.MEMBER - ), # Different case - UserAccessProperty( - user_id="USER1", role=Role.CONTRIBUTOR - ), # Different case + grants = [ + AccessGrant(id="User1", type="user", role=Role.OWNER), + AccessGrant(id="user1", type="user", role=Role.MEMBER), + AccessGrant(id="USER1", type="user", role=Role.CONTRIBUTOR), ] - # Should not throw an exception - props = KItemAccessProperties(user_access=user_access, group_access=[]) + props = KItemAccessProperties(grants=grants) - assert len(props.user_access) == 3 - assert props.by_user["User1"].role == Role.OWNER - assert props.by_user["user1"].role == Role.MEMBER - assert props.by_user["USER1"].role == Role.CONTRIBUTOR - assert props.user_by_role[Role.OWNER] == ["User1"] - assert props.user_by_role[Role.MEMBER] == ["user1"] - assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] + assert len(props.grants) == 3 + assert props.by_id["User1"].role == Role.OWNER + assert props.by_id["user1"].role == Role.MEMBER + assert props.by_id["USER1"].role == Role.CONTRIBUTOR + assert props.by_role[Role.OWNER] == ["User1"] + assert props.by_role[Role.MEMBER] == ["user1"] + assert props.by_role[Role.CONTRIBUTOR] == ["USER1"] def test_case_sensitive_ids_dict(): - """Test that IDs are case sensitive (no duplicates if different case)""" - user_access = [ - {"user_id": "User1", "role": 3}, - {"user_id": "user1", "role": 1}, # Different case - {"user_id": "USER1", "role": 2}, # Different case + """Test that IDs are case sensitive when passed as dicts""" + grants = [ + {"id": "User1", "type": "user", "role": 3}, + {"id": "user1", "type": "user", "role": 1}, + {"id": "USER1", "type": "user", "role": 2}, ] - # Should not throw an exception - props = KItemAccessProperties(user_access=user_access, group_access=[]) + props = KItemAccessProperties(grants=grants) - assert len(props.user_access) == 3 - assert props.by_user["User1"].role == Role.OWNER - assert props.by_user["user1"].role == Role.MEMBER - assert props.by_user["USER1"].role == Role.CONTRIBUTOR - assert props.user_by_role[Role.OWNER] == ["User1"] - assert props.user_by_role[Role.MEMBER] == ["user1"] - assert props.user_by_role[Role.CONTRIBUTOR] == ["USER1"] + assert len(props.grants) == 3 + assert props.by_id["User1"].role == Role.OWNER + assert props.by_id["user1"].role == Role.MEMBER + assert props.by_id["USER1"].role == Role.CONTRIBUTOR + assert props.by_role[Role.OWNER] == ["User1"] + assert props.by_role[Role.MEMBER] == ["user1"] + assert props.by_role[Role.CONTRIBUTOR] == ["USER1"] diff --git a/tests/test_access_extended.py b/tests/test_access_extended.py index 29c1d1d..a1b5d38 100644 --- a/tests/test_access_extended.py +++ b/tests/test_access_extended.py @@ -1,14 +1,13 @@ -"""Extended tests for access.py — covering gaps identified in post-merge review.""" +"""Extended tests for access.py""" import pytest from dsms.knowledge.properties.access import ( - GroupAccessProperty, + AccessGrant, KItemAccessProperties, OperationType, Role, RoleMapping, - UserAccessProperty, ) # --------------------------------------------------------------------------- @@ -110,14 +109,14 @@ def test_error_message_lists_valid_operations(): def test_serialize_role_json_mode(): - """Python mode → uppercase name string; JSON/wire mode → lowercase name string.""" - prop = UserAccessProperty(user_id="u1", role=Role.OWNER) + """Python mode -> uppercase name string; JSON/wire mode -> lowercase name string.""" + grant = AccessGrant(id="u1", type="user", role=Role.OWNER) - python_dump = prop.model_dump(mode="python") + python_dump = grant.model_dump(mode="python") assert python_dump["role"] == "OWNER" assert isinstance(python_dump["role"], str) - json_dump = prop.model_dump(mode="json") + json_dump = grant.model_dump(mode="json") assert json_dump["role"] == "owner" assert isinstance(json_dump["role"], str) @@ -130,87 +129,92 @@ def test_serialize_role_json_mode(): def test_model_dump_json_produces_string_roles(): """model_dump(mode='json') must produce lowercase string role values for the wire format.""" props = KItemAccessProperties( - user_access=[UserAccessProperty(user_id="u1", role=Role.OWNER)], - group_access=[GroupAccessProperty(group_id="g1", role=Role.MEMBER)], + grants=[ + AccessGrant(id="u1", type="user", role=Role.OWNER), + AccessGrant(id="g1", type="group", role=Role.MEMBER), + ] ) payload = props.model_dump(mode="json") - assert payload["user_access"][0]["role"] == "owner" - assert isinstance(payload["user_access"][0]["role"], str) - assert payload["group_access"][0]["role"] == "member" - assert isinstance(payload["group_access"][0]["role"], str) + user_grant = next(g for g in payload["grants"] if g["id"] == "u1") + group_grant = next(g for g in payload["grants"] if g["id"] == "g1") + assert user_grant["role"] == "owner" + assert isinstance(user_grant["role"], str) + assert group_grant["role"] == "member" + assert isinstance(group_grant["role"], str) def test_model_dump_python_produces_string_roles(): """model_dump(mode='python') must produce string role names for display.""" props = KItemAccessProperties( - user_access=[UserAccessProperty(user_id="u1", role=Role.CONTRIBUTOR)], - group_access=[], + grants=[AccessGrant(id="u1", type="user", role=Role.CONTRIBUTOR)] ) payload = props.model_dump(mode="python") - assert payload["user_access"][0]["role"] == "CONTRIBUTOR" + assert payload["grants"][0]["role"] == "CONTRIBUTOR" def test_round_trip_from_backend_dict(): """A payload as returned by the backend (string roles) must round-trip correctly.""" backend_payload = { - "user_access": [ - {"user_id": "alice", "role": "owner"}, - {"user_id": "bob", "role": "member"}, - ], - "group_access": [ - {"group_id": "dsms:internal", "role": "member"}, + "visibility": "internal", + "grants": [ + {"id": "alice", "type": "user", "role": "owner"}, + {"id": "bob", "type": "user", "role": "member"}, + {"id": "dsms:internal", "type": "group", "role": "member"}, ], } props = KItemAccessProperties(**backend_payload) - assert props.by_user["alice"].role is Role.OWNER - assert props.by_user["bob"].role is Role.MEMBER - assert props.by_group["dsms:internal"].role is Role.MEMBER + assert props.by_id["alice"].role is Role.OWNER + assert props.by_id["bob"].role is Role.MEMBER + assert props.by_id["dsms:internal"].role is Role.MEMBER + assert props.visibility == "internal" - # Serialise back and verify identity re_serialised = props.model_dump(mode="json") - assert re_serialised["user_access"][0] == { - "user_id": "alice", + assert re_serialised["grants"][0] == { + "id": "alice", + "type": "user", "role": "owner", } - assert re_serialised["group_access"][0] == { - "group_id": "dsms:internal", + assert re_serialised["grants"][2] == { + "id": "dsms:internal", + "type": "group", "role": "member", } # --------------------------------------------------------------------------- -# user_by_role property (untested in original suite) +# by_role property # --------------------------------------------------------------------------- -def test_user_by_role(): - """user_by_role must group user IDs by their Role.""" +def test_by_role(): + """by_role must group principal IDs by their Role.""" props = KItemAccessProperties( - user_access=[ - UserAccessProperty(user_id="alice", role=Role.OWNER), - UserAccessProperty(user_id="bob", role=Role.MEMBER), - UserAccessProperty(user_id="carol", role=Role.MEMBER), + grants=[ + AccessGrant(id="alice", type="user", role=Role.OWNER), + AccessGrant(id="bob", type="user", role=Role.MEMBER), + AccessGrant(id="carol", type="user", role=Role.MEMBER), ] ) - by_role = props.user_by_role + by_role = props.by_role assert by_role[Role.OWNER] == ["alice"] assert set(by_role[Role.MEMBER]) == {"bob", "carol"} assert Role.CONTRIBUTOR not in by_role -def test_user_by_role_empty(): - assert KItemAccessProperties().user_by_role == {} +def test_by_role_empty(): + assert KItemAccessProperties().by_role == {} # --------------------------------------------------------------------------- -# Validator: None inputs become empty lists +# Default grants # --------------------------------------------------------------------------- -def test_none_user_access_becomes_empty_list(): - props = KItemAccessProperties(user_access=None, group_access=None) - assert props.user_access == [] - assert props.group_access == [] +def test_empty_grants_default(): + """Default grants must be an empty list.""" + props = KItemAccessProperties() + assert props.grants == [] + assert props.visibility == "private" From 9e8cdc90624853451e28fc13ef29803ac98285f9 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 05:23:19 -0400 Subject: [PATCH 41/48] Add group-in-group API: get/add/remove subgroups --- dsms/core/dsms.py | 41 +++++++++- dsms/knowledge/utils.py | 44 +++++++++++ tests/test_groups.py | 164 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 2 deletions(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index 0865b7d..dfb0e73 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -31,10 +31,12 @@ from dsms.knowledge.utils import ( # isort:skip _add_group_member, + _add_group_to_group, _commit, _create_group, _delete_group, _get_group_members, + _get_group_subgroups, _get_kitem, _get_kitem_list, _get_ktypes_by_parent, @@ -44,6 +46,7 @@ _get_schema_data, _get_user_groups, _get_user_list, + _remove_group_from_group, _remove_group_member, _update_group, _v2_create_ktype, @@ -64,7 +67,7 @@ if TYPE_CHECKING: from dsms.core.session import Buffers - from dsms.knowledge.groups import Group, User + from dsms.knowledge.groups import Group, GroupList, User from dsms.knowledge.properties.schema_data import KItemSchemaData from dsms.knowledge.search import KItemListModel, SearchResult @@ -500,13 +503,47 @@ def add_group_member(self, group_id: str, user_id: str) -> None: def remove_group_member(self, group_id: str, user_id: str) -> None: """Remove a user from a group. - Args: group_id: The unique identifier of the group. user_id: The unique identifier of the user to remove. """ _remove_group_member(self, group_id, user_id) + def get_group_subgroups(self, group_id: str) -> "GroupList": + """Return the direct child groups of a group. + + Args: + group_id: The unique identifier of the parent group. + + Returns: + GroupList of direct child groups. + """ + return _get_group_subgroups(self, group_id) + + def add_group_to_group(self, parent_id: str, child_id: str) -> None: + """Link an existing group as a direct child of another group. + + The child group is moved in the hierarchy; its members and any of its + own subgroups are preserved. Members of the child group are resolved + recursively when listing the parent group's members. + + Args: + parent_id: The unique identifier of the parent group. + child_id: The unique identifier of the group to nest as a child. + """ + _add_group_to_group(self, parent_id, child_id) + self._user_groups = None + + def remove_group_from_group(self, parent_id: str, child_id: str) -> None: + """Detach a child group from its parent and promote it back to top-level. + + Args: + parent_id: The unique identifier of the parent group. + child_id: The unique identifier of the child group to detach. + """ + _remove_group_from_group(self, parent_id, child_id) + self._user_groups = None + def get_schema_data(self, kitem_id: str) -> "List[KItemSchemaData]": """Fetch all schema-data entries for a KItem from the remote backend. diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index f26c3ca..627f4fa 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -1757,6 +1757,50 @@ def _remove_group_member(dsms: "DSMS", group_id: str, user_id: str) -> None: ) +def _get_group_subgroups(dsms: "DSMS", group_id: str) -> List[Any]: + """Fetch the direct child groups of a group.""" + from dsms.knowledge.groups import Group, GroupList + + response = _perform_request( + dsms, + f"api/users/groups/{group_id}/subgroups", + "get", + ) + if not response.ok: + raise ConnectionError( + f"Failed to fetch subgroups of group {group_id}: {response.text}" + ) + return GroupList([Group(**g) for g in response.json()]) + + +def _add_group_to_group(dsms: "DSMS", parent_id: str, child_id: str) -> None: + """Link an existing group as a subgroup of another group.""" + response = _perform_request( + dsms, + f"api/users/groups/{parent_id}/subgroups/{child_id}", + "post", + ) + if not response.ok: + raise ValueError( + f"Failed to link group {child_id} under group {parent_id}: {response.text}" + ) + + +def _remove_group_from_group( + dsms: "DSMS", parent_id: str, child_id: str +) -> None: + """Detach a child group from its parent, promoting it back to top-level.""" + response = _perform_request( + dsms, + f"api/users/groups/{parent_id}/subgroups/{child_id}", + "delete", + ) + if not response.ok: + raise ValueError( + f"Failed to unlink group {child_id} from group {parent_id}: {response.text}" + ) + + def _get_user_list(dsms: "DSMS"): """Fetch all users from the DSMS backend.""" from dsms.knowledge.groups import User, UserList diff --git a/tests/test_groups.py b/tests/test_groups.py index 3ff0b9b..03f7f30 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -775,6 +775,170 @@ def test_remove_group_member_raises_on_error(custom_address): dsms.remove_group_member("grp-1", "u-bad") +# --------------------------------------------------------------------------- +# Group-in-group: get_group_subgroups / add_group_to_group / remove_group_from_group +# --------------------------------------------------------------------------- + +MOCK_SUBGROUPS = [ + {"id": "grp-child-1", "name": "Child A"}, + {"id": "grp-child-2", "name": "Child B"}, +] + + +@responses_lib.activate +def test_get_group_subgroups_returns_grouplist(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups/grp-1/subgroups"), + json=MOCK_SUBGROUPS, + status=200, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + subs = dsms.get_group_subgroups("grp-1") + assert isinstance(subs, GroupList) + assert len(subs) == 2 + assert subs.by_id["grp-child-1"].name == "Child A" + assert subs.by_id["grp-child-2"].name == "Child B" + + +@responses_lib.activate +def test_get_group_subgroups_raises_on_error(custom_address): + responses_lib.add( + responses_lib.GET, + urljoin(custom_address, "api/users/groups/bad-id/subgroups"), + json={"detail": "Not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ConnectionError, match="bad-id"): + dsms.get_group_subgroups("bad-id") + + +@responses_lib.activate +def test_add_group_to_group(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin( + custom_address, "api/users/groups/grp-parent/subgroups/grp-child" + ), + status=204, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + dsms.add_group_to_group("grp-parent", "grp-child") + + assert len(responses_lib.calls) == 1 + assert ( + "groups/grp-parent/subgroups/grp-child" + in responses_lib.calls[0].request.url + ) + # cache must be invalidated + assert dsms._user_groups is None + + +@responses_lib.activate +def test_add_group_to_group_raises_on_error(custom_address): + responses_lib.add( + responses_lib.POST, + urljoin( + custom_address, "api/users/groups/grp-parent/subgroups/bad-child" + ), + json={"detail": "Group not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="bad-child"): + dsms.add_group_to_group("grp-parent", "bad-child") + + +@responses_lib.activate +def test_remove_group_from_group(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin( + custom_address, "api/users/groups/grp-parent/subgroups/grp-child" + ), + status=204, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + dsms.remove_group_from_group("grp-parent", "grp-child") + + assert len(responses_lib.calls) == 1 + assert ( + "groups/grp-parent/subgroups/grp-child" + in responses_lib.calls[0].request.url + ) + assert dsms._user_groups is None + + +@responses_lib.activate +def test_remove_group_from_group_raises_on_error(custom_address): + responses_lib.add( + responses_lib.DELETE, + urljoin( + custom_address, "api/users/groups/grp-parent/subgroups/bad-child" + ), + json={"detail": "Group not found"}, + status=404, + ) + + with pytest.warns(UserWarning): + from dsms.core.dsms import DSMS + + dsms = DSMS( + host_url=custom_address, + ping_backend=False, + auto_fetch_ktypes=False, + ) + + with pytest.raises(ValueError, match="bad-child"): + dsms.remove_group_from_group("grp-parent", "bad-child") + + # --------------------------------------------------------------------------- # GroupListBase removed — flat returns a plain list of BaseGroup # --------------------------------------------------------------------------- From 2f3e8fe0078a853dfc898a65572dacec605ba9b9 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 06:58:51 -0400 Subject: [PATCH 42/48] Widen fuzzy field type to accept string values (e.g. 'id' for exact UUID match) --- dsms/knowledge/search.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dsms/knowledge/search.py b/dsms/knowledge/search.py index 575092e..6409367 100644 --- a/dsms/knowledge/search.py +++ b/dsms/knowledge/search.py @@ -17,10 +17,11 @@ class KItemSearchResult(BaseModel): kitem: Union["KItem", "KItemCompactedModel"] = Field( ..., description="KItem returned by the search" ) - fuzzy: Union[bool, float] = Field( + fuzzy: Union[bool, float, str] = Field( ..., description="""Whether the KItem was found through a similarity hit. - If not a bool, a float indicates the distance from search term""", + If not a bool, a float indicates the distance from search term. + The string 'id' indicates the KItem was found by exact UUID lookup.""", ) def __str__(self): From 8c8ba77fe8c5e8d71f3ef6485c84d317aba48d33 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 08:59:03 -0400 Subject: [PATCH 43/48] Remove legacy INTERNAL_GROUP / PUBLIC_GROUP constants - Delete dsms/knowledge/groups/public.py (never deployed) - Remove INTERNAL_GROUP, PUBLIC_GROUP, refresh_public_groups from groups __init__ - Remove id_internal, id_public, label_internal, label_public from BaseConfiguration - Remove refresh_public_groups call from DSMS.__init__ --- dsms/core/configuration.py | 20 -------------- dsms/core/dsms.py | 4 --- dsms/knowledge/groups/__init__.py | 8 ------ dsms/knowledge/groups/public.py | 44 ------------------------------- 4 files changed, 76 deletions(-) delete mode 100644 dsms/knowledge/groups/public.py diff --git a/dsms/core/configuration.py b/dsms/core/configuration.py index 1f26f05..229fcc4 100644 --- a/dsms/core/configuration.py +++ b/dsms/core/configuration.py @@ -46,26 +46,6 @@ class Loglevel(Enum): class BaseConfiguration(BaseSettings): """Base Configuration for DSMS-SDK""" - label_internal: str = Field( - "Internal", - description="Label to use for the internal visibility group.", - ) - - label_public: str = Field( - "Public", - description="Label to use for the public visibility group.", - ) - - id_internal: str = Field( - "dsms:internal", - description="ID of the special group that grants read access to all authenticated users.", - ) - - id_public: str = Field( - "dsms:public", - description="ID of the special group that grants read access to all users.", - ) - model_config = ConfigDict(use_enum_values=True) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index dfb0e73..d5db615 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -139,10 +139,6 @@ def __init__( Please specify kwargs for to be passed to the `Configuration`-object _OR_ an instance of this `Configuration`-object directly.""") - from dsms.knowledge.groups.public import refresh_public_groups - - refresh_public_groups(self.config) - self._sparql_interface = SparqlInterface(self) if self.config.auto_fetch_ktypes: _get_remote_ktypes(self) diff --git a/dsms/knowledge/groups/__init__.py b/dsms/knowledge/groups/__init__.py index 6c767a6..7f2af40 100644 --- a/dsms/knowledge/groups/__init__.py +++ b/dsms/knowledge/groups/__init__.py @@ -1,18 +1,10 @@ """DSMS User Groups Module.""" from .models import BaseGroup, Group, GroupList, User, UserList -from .public import ( - INTERNAL_GROUP, - PUBLIC_GROUP, - refresh_public_groups, -) __all__ = [ "Group", "GroupList", - "INTERNAL_GROUP", - "PUBLIC_GROUP", - "refresh_public_groups", "User", "BaseGroup", "UserList", diff --git a/dsms/knowledge/groups/public.py b/dsms/knowledge/groups/public.py deleted file mode 100644 index b63620e..0000000 --- a/dsms/knowledge/groups/public.py +++ /dev/null @@ -1,44 +0,0 @@ -"""DSMS Public User Groups Module.""" - -from dsms.core.configuration import BaseConfiguration -from dsms.core.session import Session - -from .models import Group - -# The internally/externally public group objects will generally -# be served by the user-service, but we define them here for uniquely -# setting the ids and names in a common place. -# They can be adapted through environment variables anyway. -# A common place is needed because the group objects are used in various places -# such as internally within the knowledge service, the user service, and the SDK itself. -# If the IDs and names of these public groups are only delivered in the user service, -# we cannot distinguish them from the ones which come from keycloak -# - indicating only organizational groups. -# -# NOTE: These constants are initialised at import time using whatever config is -# available then (env-vars or defaults). If a DSMS instance is later created -# with a Configuration that overrides id_internal / id_public, -# call refresh_public_groups(config) to keep the constants in sync. - - -def _make_public_groups(cfg=None): - if cfg is None: - cfg = Session.dsms.config if Session.dsms else BaseConfiguration() - return ( - Group(id=cfg.id_internal, name=cfg.label_internal), - Group(id=cfg.id_public, name=cfg.label_public), - ) - - -INTERNAL_GROUP, PUBLIC_GROUP = _make_public_groups() - - -def refresh_public_groups(config=None) -> None: - """Re-create the public group constants from the given (or current) config. - - Call this after constructing a DSMS instance whose Configuration overrides - id_internal or id_public so that the module-level constants stay in sync - with the running configuration. - """ - global INTERNAL_GROUP, PUBLIC_GROUP - INTERNAL_GROUP, PUBLIC_GROUP = _make_public_groups(config) From 8a1f3609d5c5ca07fabbdb97f2a3c37561ca3228 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 10:24:02 -0400 Subject: [PATCH 44/48] Remove stale public group constant tests Follow-up to 8c8ba77: test_groups.py still imported from the deleted dsms/knowledge/groups/public module. Drop the import and the 6 tests that covered those constants. --- tests/test_groups.py | 63 +------------------------------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/tests/test_groups.py b/tests/test_groups.py index 03f7f30..07d5b20 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -1,4 +1,4 @@ -"""Tests for groups models, public group constants, and DSMS user/group API.""" +"""Tests for groups models and DSMS user/group API.""" from urllib.parse import urljoin @@ -12,10 +12,6 @@ User, UserList, ) -from dsms.knowledge.groups.public import ( - INTERNAL_GROUP, - PUBLIC_GROUP, -) # --------------------------------------------------------------------------- # Group model @@ -156,63 +152,6 @@ def test_userlist_getitem_missing_raises(): _ = ul["nonexistent"] -# --------------------------------------------------------------------------- -# Public group constants -# --------------------------------------------------------------------------- - - -def test_internal_group_id(): - """INTERNAL_GROUP.id must match the BaseConfiguration default.""" - assert INTERNAL_GROUP.id == "dsms:internal" - - -def test_public_group_id(): - assert PUBLIC_GROUP.id == "dsms:public" - - -def test_internal_group_has_name(): - assert INTERNAL_GROUP.name != "" - - -def test_public_group_has_name(): - assert PUBLIC_GROUP.name != "" - - -def test_refresh_public_groups_uses_custom_config(): - """refresh_public_groups(config) must update the module-level constants.""" - from dsms.core.configuration import BaseConfiguration - from dsms.knowledge.groups import public as pub - - original_id = pub.INTERNAL_GROUP.id - - custom_cfg = BaseConfiguration( - id_internal="custom:internal", - id_public="custom:external", - label_internal="Custom Internal", - label_public="Custom External", - ) - pub.refresh_public_groups(custom_cfg) - - assert pub.INTERNAL_GROUP.id == "custom:internal" - assert pub.PUBLIC_GROUP.id == "custom:external" - assert pub.INTERNAL_GROUP.name == "Custom Internal" - - # Restore defaults so other tests are not affected - pub.refresh_public_groups() - assert pub.INTERNAL_GROUP.id == original_id - - -def test_refresh_public_groups_without_arg_restores_defaults( - reset_dsms_session, -): - """refresh_public_groups() with no argument should use env/defaults.""" - from dsms.knowledge.groups import public as pub - - pub.refresh_public_groups() - assert pub.INTERNAL_GROUP.id == "dsms:internal" - assert pub.PUBLIC_GROUP.id == "dsms:public" - - # --------------------------------------------------------------------------- # DSMS.user_groups, DSMS.users, DSMS.get_user — caching and HTTP behaviour # --------------------------------------------------------------------------- From be3492dc7e1bfc2db6041a3d93fe0b5a9c153daf Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 12:14:37 -0400 Subject: [PATCH 45/48] Add visibility filter to search - Add optional visibility parameter to dsms.search() and _search() - Accepts 'private', 'internal', or 'public'; defaults to None (no filter) - Passes visibility in the POST payload to the backend search endpoint --- dsms/core/dsms.py | 4 +++- dsms/knowledge/utils.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dsms/core/dsms.py b/dsms/core/dsms.py index d5db615..d9b2106 100644 --- a/dsms/core/dsms.py +++ b/dsms/core/dsms.py @@ -3,7 +3,7 @@ import os import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union from uuid import UUID from dotenv import load_dotenv @@ -244,6 +244,7 @@ def search( compact: "Optional[bool]" = False, contexts: "Optional[List[str]]" = None, attachment_extensions: "Optional[List[str]]" = None, + visibility: "Optional[Literal['private', 'internal', 'public']]" = None, ) -> "List[SearchResult]": """Search for KItems in the remote backend.""" return _search( @@ -257,6 +258,7 @@ def search( compact, contexts, attachment_extensions, + visibility, ) @property diff --git a/dsms/knowledge/utils.py b/dsms/knowledge/utils.py index 627f4fa..d9c7b7b 100644 --- a/dsms/knowledge/utils.py +++ b/dsms/knowledge/utils.py @@ -10,7 +10,7 @@ import warnings from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union from uuid import UUID import oyaml as yaml @@ -1074,6 +1074,7 @@ def _search( compact: "Optional[bool]" = False, contexts: "Optional[List[str]]" = None, attachment_extensions: "Optional[List[str]]" = None, + visibility: Optional[Literal["private", "internal", "public"]] = None, ) -> "List[SearchResult]": """Search for KItems in the remote backend""" from dsms import KItem, KItemCompactedModel @@ -1091,6 +1092,8 @@ def _search( } if contexts is not None: payload["contexts"] = contexts + if visibility is not None: + payload["visibility"] = visibility params = {"allow_fuzzy": allow_fuzzy} if attachment_extensions is not None: params["attachment_extensions"] = attachment_extensions From 082192a7c2d51073656858caa2fe459e21fd1781 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 9 Jun 2026 13:19:38 -0400 Subject: [PATCH 46/48] Fix AttributeError when choices is None in validate_custom_property_entry Guard the error_message construction with the select_options check so choices.keys() is never called on None when no select options are defined. --- dsms/knowledge/kitem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index 3bbd412..6adb2ee 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -637,12 +637,12 @@ def validate_custom_property_entry( ) and entry.value is not None ): - error_message = """Value `{}` is not a valid select option. - Valid options are: """ + str(list(choices.keys())) + "\n" if not select_options: raise ValueError( f"Widget of type `{entry.type}` does not have select options." ) + error_message = """Value `{}` is not a valid select option. + Valid options are: """ + str(list(choices.keys())) + "\n" if isinstance(entry.value, str): if entry.value not in choices: raise ValueError(error_message.format(entry.value)) From ee03af0d909870777330a677428c1ae1485729a1 Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 21 Jul 2026 10:03:58 -0400 Subject: [PATCH 47/48] Add KItem.populate_schema and semantic-schemas integration Adds KItem.populate_schema() to apply simplified-input transforms from k-type semantic schema specs, schema_to_oold() in the semantics module, a shorthand dict syntax for schema_data on KItem construction, and a tutorial notebook demonstrating the full workflow. --- .../tutorials/9_semantic_schemas.ipynb | 311 ++++++++++++++++++ dsms/knowledge/kitem.py | 111 +++++++ dsms/knowledge/semantics/__init__.py | 57 +++- setup.cfg | 1 + 4 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 docs/dsms_sdk/tutorials/9_semantic_schemas.ipynb diff --git a/docs/dsms_sdk/tutorials/9_semantic_schemas.ipynb b/docs/dsms_sdk/tutorials/9_semantic_schemas.ipynb new file mode 100644 index 0000000..27d44ee --- /dev/null +++ b/docs/dsms_sdk/tutorials/9_semantic_schemas.ipynb @@ -0,0 +1,311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 9. Semantic Schemas\n", + "\n", + "This tutorial shows how to attach semantic schema data to a KItem using the SDK.\n", + "Semantic schemas are defined in the k-type spec and map a KItem's scientific content\n", + "to ontology-typed RDF nodes. They are distinct from `custom_properties`, which drive\n", + "the UI form.\n", + "\n", + "By the end of this tutorial you will know:\n", + "\n", + "- what `custom_properties` and `schema_data` each represent,\n", + "- how to populate both at construction time using the same input style,\n", + "- how to use the `populate_schema()` method for post-construction updates." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.1. Setting up\n", + "\n", + "Before you run this tutorial: make sure to have access to a DSMS-instance of your\n", + "interest, along with installation of this package, and have established access to the\n", + "DSMS through DSMS-SDK (refer to\n", + "[Connecting to DSMS](../dsms_sdk.md#connecting-to-dsms)).\n", + "\n", + "Import the needed classes and functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from dsms import DSMS, KItem\n", + "\n", + "dsms = DSMS(env=\".env\") if os.path.exists(\".env\") else DSMS()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.2. Custom properties vs. semantic schemas\n", + "\n", + "A KItem can carry two complementary representations of its data:\n", + "\n", + "| | `custom_properties` | `schema_data` |\n", + "|---|---|---|\n", + "| **Purpose** | UI form displayed on the platform | OO-LD / RDF graph for SPARQL and semantic reasoning |\n", + "| **Defined by** | K-type webform schema | K-type semantic schema spec (v2) |\n", + "| **Input format** | Flat dict keyed by field label | Flat dict keyed by transform field names |\n", + "| **Set at init?** | Yes | Yes |\n", + "\n", + "Fields such as `name` appear in both representations and should be kept consistent.\n", + "Geometry and administrative metadata typically live in `custom_properties`;\n", + "measurement results, provenance, and ontology-typed relationships live in `schema_data`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.3. Discovering available semantic schemas\n", + "\n", + "The k-type spec lists which semantic schemas a KItem of that type can carry.\n", + "Fetch the v2 spec for any k-type to see the available schema IDs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ktype_v2 = dsms.get_v2_ktype(\"tensile-test\")\n", + "for s in ktype_v2.spec.resolved_semantic_schemas:\n", + " print(s.id, \"\u2192\", s.url)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.4. Setting both at construction time\n", + "\n", + "Both `custom_properties` and `schema_data` accept a plain dict at construction.\n", + "For `schema_data`, pass `{schema_id: simplified_input_dict}`. The schema transform\n", + "is fetched and applied immediately; subsequent constructions with the same schema\n", + "URL use a process-level cache, keeping the cost comparable to `custom_properties`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "specimen = KItem(\n", + " name=\"Specimen-TT-01\",\n", + " ktype_id=dsms.ktypes.Specimen,\n", + " custom_properties={\n", + " \"Specimen type\": \"flat\",\n", + " \"Width\": 12.5,\n", + " \"Length\": 80.0,\n", + " \"Thickness\": 1.5,\n", + " },\n", + " schema_data={\n", + " \"specimen/PMDCo\": {\n", + " \"label\": \"Specimen-TT-01\",\n", + " \"width_mm\": 12.5,\n", + " \"length_mm\": 80.0,\n", + " \"thickness_mm\": 1.5,\n", + " }\n", + " },\n", + ")\n", + "\n", + "specimen" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Commit to the platform. The `schema_data` is already resolved to OO-LD\n", + "and will be persisted as-is.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dsms.add(specimen)\n", + "dsms.commit()\n", + "specimen.url" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `schema_data` already holds the resolved OO-LD content right after\n", + "construction \u2014 no need to wait for a commit to inspect it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for entry in specimen.schema_data:\n", + " print(\"schema_id:\", entry.schema_id)\n", + " print(\"content keys:\", list(entry.content.keys()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.5. Using `populate_schema()` for post-construction updates\n", + "\n", + "`populate_schema(schema_id, input_data)` is the method-call equivalent. Use it\n", + "when you need to add or replace a schema entry after the KItem has been constructed,\n", + "or when you want explicit control over the transform step.\n", + "\n", + "It returns `self`, so calls can be chained." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tensile_test = KItem(\n", + " name=\"TensileTest-01\",\n", + " ktype_id=dsms.ktypes.TensileTest,\n", + " custom_properties={\n", + " \"Identifier\": \"TT-2024-001\",\n", + " \"Start time\": \"2024-03-15T09:00:00\",\n", + " \"End time\": \"2024-03-15T09:45:00\",\n", + " },\n", + ")\n", + "\n", + "tensile_test.populate_schema(\n", + " \"characterization/tensile-test/TTO\",\n", + " {\n", + " \"test_name\": \"TT-2024-001\",\n", + " \"specimen_iri\": str(specimen.id),\n", + " \"results\": [\n", + " {\"property\": \"YieldStrength\", \"value\": 350.0, \"unit\": \"MPa\"},\n", + " {\"property\": \"UltimateTensileStrength\", \"value\": 490.0, \"unit\": \"MPa\"},\n", + " {\"property\": \"Elongation\", \"value\": 28.5, \"unit\": \"%\"},\n", + " ],\n", + " },\n", + ")\n", + "\n", + "dsms.add(tensile_test)\n", + "dsms.commit()\n", + "tensile_test.url" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.6. Inspecting the RDF subgraph\n", + "\n", + "The platform generates an RDF subgraph from `schema_data` asynchronously after\n", + "each commit. Use `kitem.subgraph` to retrieve and inspect it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "time.sleep(3) # allow server-side graph generation to complete\n", + "\n", + "try:\n", + " print(tensile_test.subgraph.serialize(format=\"turtle\"))\n", + "except ValueError:\n", + " print(\"Subgraph not yet available \u2014 retry in a moment.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.7. Updating a schema entry\n", + "\n", + "Calling `populate_schema()` with the same `schema_id` replaces the existing entry.\n", + "The dict shorthand at construction works the same way." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tensile_test.populate_schema(\n", + " \"characterization/tensile-test/TTO\",\n", + " {\n", + " \"test_name\": \"TT-2024-001\",\n", + " \"specimen_iri\": str(specimen.id),\n", + " \"results\": [\n", + " {\"property\": \"YieldStrength\", \"value\": 355.0, \"unit\": \"MPa\"},\n", + " {\"property\": \"UltimateTensileStrength\", \"value\": 495.0, \"unit\": \"MPa\"},\n", + " {\"property\": \"Elongation\", \"value\": 29.0, \"unit\": \"%\"},\n", + " ],\n", + " },\n", + ")\n", + "\n", + "dsms.add(tensile_test)\n", + "dsms.commit()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9.8. Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "del dsms[tensile_test]\n", + "del dsms[specimen]\n", + "dsms.commit()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index 6adb2ee..ab09352 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -16,6 +16,7 @@ ValidationInfo, field_validator, field_serializer, + model_validator, ) from dsms.core.logging import handler # isort:skip @@ -454,6 +455,42 @@ def validate_custom_properties( cls.validate_custom_property_entry(entry, ktype) return value + @field_validator("schema_data", mode="before") + @classmethod + def _coerce_schema_data( + cls, + value: "Optional[Any]", + ) -> "Optional[Any]": + """Accept a plain dict ``{schema_id: simplified_input}`` as shorthand.""" + if isinstance(value, dict): + return [ + {"schema_id": sid, "content": {"__simplified__": data}} + for sid, data in value.items() + ] + return value + + @model_validator(mode="after") + def _resolve_schema_data_shorthands(self) -> "KItem": + """Immediately resolve any ``{schema_id: simplified_input}`` shorthands. + + After all field validators have run, the DSMS session is available via + :attr:`dsms`. Any ``schema_data`` entry whose content carries the + ``__simplified__`` sentinel is transformed to OO-LD here, so that + ``schema_data`` always contains valid OO-LD by the time the caller + receives the constructed object. + """ + if not self.schema_data: + return self + pending = [ + (entry.schema_id, entry.content["__simplified__"]) + for entry in self.schema_data + if isinstance(entry.content, dict) + and "__simplified__" in entry.content + ] + for schema_id, input_data in pending: + self.populate_schema(schema_id, input_data) + return self + @field_validator("contexts") def _validate_contexts( cls, @@ -813,3 +850,77 @@ def is_a(self, to_be_compared: KType) -> bool: def refresh(self) -> None: """Refresh the KItem""" _refresh_kitem(self) + + def populate_schema( + self, schema_id: str, input_data: "Dict[str, Any]" + ) -> "KItem": + """Populate a semantic schema instance on this KItem. + + Looks up *schema_id* in the k-type spec's ``resolved_semantic_schemas`` + list, fetches the schema's ``transform.simplified.jsonata`` file (if it + exists), applies the transform to *input_data* to produce an OO-LD + document, and stores the result in :attr:`schema_data`. + + For schemas **with** a ``transform.simplified.jsonata``, pass the + schema's simplified input format in *input_data* (e.g. ``test_name``, + ``specimen_iri``, ``results`` for ``characterization/tensile-test/TTO``). + + For schemas **without** a transform (e.g. ``dataset/generic/DCAT``), + pass OO-LD directly. + + Call :meth:`DSMS.add` and :meth:`DSMS.commit` afterwards to persist + the schema data to the platform. + + Args: + schema_id: Schema identifier exactly matching the ``id`` field in + the k-type spec's ``semantic_schemas`` list, e.g. + ``"characterization/tensile-test/TTO"``. + input_data: Simplified input dict (schemas with transform) or OO-LD + dict (schemas without transform). + + Returns: + ``self`` to allow method chaining. + + Raises: + ValueError: If *schema_id* is not listed in the k-type spec, or if + the k-type has no v2 spec. + RuntimeError: If the schema cannot be fetched or the transform fails. + """ + from dsms.knowledge.semantics import schema_to_oold + + ktype_v2 = self.dsms.get_v2_ktype(str(self.ktype_id)) + if not ktype_v2 or not ktype_v2.spec: + raise ValueError( + f"K-type '{self.ktype_id}' has no v2 spec. " + "Import or create the k-type spec before calling populate_schema()." + ) + + schemas = ( + ktype_v2.spec.resolved_semantic_schemas + or ktype_v2.spec.semantic_schemas + or [] + ) + schema_ref = next((s for s in schemas if s.id == schema_id), None) + if schema_ref is None: + valid = [s.id for s in schemas] + raise ValueError( + f"Schema ID '{schema_id}' is not listed in the k-type spec for " + f"'{self.ktype_id}'. Available schema IDs: {valid}" + ) + + oold_doc = schema_to_oold(schema_ref.url, input_data) + + new_entry = KItemSchemaData(schema_id=schema_id, content=oold_doc) + + if self.schema_data is None: + self.schema_data = [new_entry] + else: + existing_ids = [sd.schema_id for sd in self.schema_data] + if schema_id in existing_ids: + idx = existing_ids.index(schema_id) + self.schema_data = list(self.schema_data) + self.schema_data[idx] = new_entry + else: + self.schema_data = list(self.schema_data) + [new_entry] + + return self diff --git a/dsms/knowledge/semantics/__init__.py b/dsms/knowledge/semantics/__init__.py index 3c31da6..76cf43c 100644 --- a/dsms/knowledge/semantics/__init__.py +++ b/dsms/knowledge/semantics/__init__.py @@ -1 +1,56 @@ -"""DSMS Semantics Module""" +"""DSMS Semantics Module + +Exposes :func:`schema_to_oold`, used by :meth:`KItem.populate_schema` to +convert simplified input dicts to OO-LD documents via the semantic schema +transforms defined in the k-type spec. +""" + +import logging +from typing import Any, Dict + +logger = logging.getLogger(__name__) + + +def schema_to_oold( + schema_url: str, input_data: Dict[str, Any] +) -> Dict[str, Any]: + """Convert a simplified input dict to an OO-LD document. + + Loads the schema identified by *schema_url* using + :meth:`semantic_schemas.Schema.from_url`, then applies its + ``transform.simplified.jsonata`` transform to *input_data*. + + If the schema has no simplified transform, *input_data* is returned as-is + (the caller is expected to pass OO-LD directly in that case). + + Args: + schema_url: A GitHub tree URL pointing to the schema folder, as stored + in the k-type spec's ``semantic_schemas[].url`` field. + input_data: Simplified input dict (for schemas with a transform) or an + OO-LD dict (for schemas without a transform). + + Returns: + OO-LD document dict ready to be stored as ``KItemSchemaData.content``. + + Raises: + ImportError: If the ``semantic-schemas`` package is not installed. + RuntimeError: If the schema YAML cannot be fetched. + """ + try: + from semantic_schemas import Schema + except ImportError as exc: + raise ImportError( + "The 'semantic-schemas' package is required to apply simplified " + "input transforms. Install it with: pip install semantic-schemas" + ) from exc + + schema = Schema.from_url(schema_url) + + if schema._transform_src is None: # pylint: disable=protected-access + logger.debug( + "No simplified transform found at %s — treating input as OO-LD", + schema_url, + ) + return input_data + + return schema.transform(input_data) diff --git a/setup.cfg b/setup.cfg index 5f30961..22023ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = rdflib>=6,<8 requests segno>=1.6,<2 + semantic-schemas>=0.7 python_requires = >=3.10,<3.15 include_package_data = True From c1ca6a8668dceeb49e4981f23b36016a235fb85a Mon Sep 17 00:00:00 2001 From: Yoav Nahshon Date: Tue, 21 Jul 2026 11:06:26 -0400 Subject: [PATCH 48/48] Fix CI: correct schema_to_oold, upgrade pylint, sync pre-commit to system env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related fixes: - schema_to_oold: replace non-existent Schema.from_url with a direct requests fetch of specs/transform.simplified.jsonata from the raw GitHub URL; fall back to pass-through on HTTP 404 - setup.cfg: upgrade pylint 3.2.0→3.3.9 so too-many-positional-arguments in .pylintrc is recognised (added in pylint 3.3.0) - .pre-commit-config.yaml: switch pylint hook from language: python to language: system so local runs use the same installed environment as CI, eliminating stale-cache version drift --- .pre-commit-config.yaml | 2 +- dsms/knowledge/semantics/__init__.py | 28 ++++++++++++++++------------ setup.cfg | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd6fa5c..d076a78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: name: pylint entry: pylint args: ["--rcfile=.pylintrc", "--extension-pkg-whitelist=pydantic"] - language: python + language: system types: [python] require_serial: true files: ^(dsms)/.* diff --git a/dsms/knowledge/semantics/__init__.py b/dsms/knowledge/semantics/__init__.py index 76cf43c..786d284 100644 --- a/dsms/knowledge/semantics/__init__.py +++ b/dsms/knowledge/semantics/__init__.py @@ -8,6 +8,8 @@ import logging from typing import Any, Dict +import requests + logger = logging.getLogger(__name__) @@ -16,15 +18,13 @@ def schema_to_oold( ) -> Dict[str, Any]: """Convert a simplified input dict to an OO-LD document. - Loads the schema identified by *schema_url* using - :meth:`semantic_schemas.Schema.from_url`, then applies its - ``transform.simplified.jsonata`` transform to *input_data*. - - If the schema has no simplified transform, *input_data* is returned as-is - (the caller is expected to pass OO-LD directly in that case). + Fetches ``specs/transform.simplified.jsonata`` from *schema_url* (a raw + GitHub base URL) and applies the JSONata transform to *input_data*. If + the transform file does not exist (HTTP 404), *input_data* is returned + as-is — the caller is expected to pass OO-LD directly in that case. Args: - schema_url: A GitHub tree URL pointing to the schema folder, as stored + schema_url: Raw GitHub URL pointing to the schema folder, as stored in the k-type spec's ``semantic_schemas[].url`` field. input_data: Simplified input dict (for schemas with a transform) or an OO-LD dict (for schemas without a transform). @@ -34,23 +34,27 @@ def schema_to_oold( Raises: ImportError: If the ``semantic-schemas`` package is not installed. - RuntimeError: If the schema YAML cannot be fetched. + requests.HTTPError: If fetching the transform file fails (non-404). """ try: - from semantic_schemas import Schema + from jsonata.jsonata import Jsonata except ImportError as exc: raise ImportError( "The 'semantic-schemas' package is required to apply simplified " "input transforms. Install it with: pip install semantic-schemas" ) from exc - schema = Schema.from_url(schema_url) + transform_url = ( + f"{schema_url.rstrip('/')}/specs/transform.simplified.jsonata" + ) + response = requests.get(transform_url, timeout=30) - if schema._transform_src is None: # pylint: disable=protected-access + if response.status_code == 404: logger.debug( "No simplified transform found at %s — treating input as OO-LD", schema_url, ) return input_data - return schema.transform(input_data) + response.raise_for_status() + return Jsonata(response.text).evaluate(input_data) diff --git a/setup.cfg b/setup.cfg index 22023ea..523ecbd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,7 +58,7 @@ docs = sphinxcontrib-redoc==1.6.0 pre_commit = pre-commit==3.3.2 - pylint==3.2.0 + pylint==3.3.9 tests = pytest>=7.4.3 pytest-mock