From fd55ecfdefeb8ffb7096312bd9832b40b6644865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com> Date: Thu, 14 Mar 2024 20:08:46 +0100 Subject: [PATCH 01/10] MAINT: remove error when comparing different roles --- src/linkahead/apiutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index e2ed0fac..06547198 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -215,9 +215,9 @@ def compare_entities(old_entity: Entity, new_entity: Entity, compare_referenced_ if old_entity is new_entity: return (olddiff, newdiff) - if type(old_entity) is not type(new_entity): - raise ValueError( - "Comparison of different Entity types is not supported.") + #if type(old_entity) is not type(new_entity): + # raise ValueError( + # "Comparison of different Entity types is not supported.") for attr in SPECIAL_ATTRIBUTES: try: -- GitLab From 5f1b683541119becabfffb44a0dd7be3b58f5458 Mon Sep 17 00:00:00 2001 From: Daniel Hornung <d.hornung@indiscale.com> Date: Mon, 25 Mar 2024 10:13:51 +0100 Subject: [PATCH 02/10] DOC: Typo --- src/linkahead/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 6f6e4c8f..d7bc7a47 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -2286,7 +2286,7 @@ class _Properties(list): class _ParentList(list): - # TODO unclear why this class is private. Isn't it use full for users? + # TODO unclear why this class is private. Isn't it useful for users? def _get_entity_by_cuid(self, cuid): ''' -- GitLab From 6a39b7348ef05682e98e2aa6a7a7506c63d66bcf Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Thu, 2 May 2024 14:22:22 +0200 Subject: [PATCH 03/10] WIP: Plain json serialization for high level api. --- src/linkahead/high_level_api.py | 91 ++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py index 18d219c7..5b1682f3 100644 --- a/src/linkahead/high_level_api.py +++ b/src/linkahead/high_level_api.py @@ -471,8 +471,7 @@ class CaosDBPythonEntity(object): if isinstance(att, list): return att - else: - return [att] + return [att] def add_parent(self, parent: Union[ CaosDBPythonUnresolvedParent, "CaosDBPythonRecordType", str]): @@ -679,53 +678,68 @@ class CaosDBPythonEntity(object): return entity - def serialize(self, without_metadata: bool = False, visited: dict = None): - """ - Serialize necessary information into a dict. - - without_metadata: bool - If True don't set the metadata field in order to increase - readability. Not recommended if deserialization is needed. + def serialize(self, without_metadata: bool = None, plain_json: bool = False, + visited: dict = None): + """Serialize necessary information into a dict. + +Parameters +---------- +without_metadata: bool, optional + If True don't set the metadata field in order to increase + readability. Not recommended if deserialization is needed. +plain_json: bool, optional + If True, serialize to a plain dict without any additional information besides the property values, + name and id. This should conform to the format as specified by the json schema generated by the + advanced user tools. This implies ``without_metadata = True``. """ + if plain_json: + if without_metadata is None: + without_metadata = True + if not without_metadata: + raise ValueError("`plain_json` implies `without_metadata`.") + if without_metadata is None: + without_metadata = False if visited is None: - visited = dict() + visited = {} if self in visited: return visited[self] - metadata: Dict[str, Any] = dict() - properties = dict() - parents = list() + metadata: Dict[str, Any] = {} + properties = {} + parents = [] # The full information to be returned: - fulldict = dict() + fulldict = {} visited[self] = fulldict - # Add CaosDB role: - fulldict["role"] = standard_type_for_high_level_type(self, True) - for parent in self._parents: if isinstance(parent, CaosDBPythonEntity): - parents.append(parent.serialize(without_metadata, visited)) + parents.append(parent.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited)) elif isinstance(parent, CaosDBPythonUnresolvedParent): parents.append({"name": parent.name, "id": parent.id, "unresolved": True}) else: raise RuntimeError("Incompatible class used as parent.") - for baseprop in ("name", "id", "description", "version"): - val = self.__getattribute__(baseprop) - if val is not None: - fulldict[baseprop] = val + if not plain_json: + # Add LinkAhead role: + fulldict["role"] = standard_type_for_high_level_type(self, True) + for baseprop in ("name", "id", "description", "version"): + val = self.__getattribute__(baseprop) + if val is not None: + fulldict[baseprop] = val - if type(self) == CaosDBPythonFile: - fulldict["file"] = self.file - fulldict["path"] = self.path + if isinstance(self, CaosDBPythonFile): + fulldict["file"] = self.file + fulldict["path"] = self.path for p in self.get_properties(): m = self.get_property_metadata(p) - metadata[p] = dict() + metadata[p] = {} for f in fields(m): val = m.__getattribute__(f.name) if val is not None: @@ -735,30 +749,37 @@ class CaosDBPythonEntity(object): if isinstance(val, CaosDBPythonUnresolvedReference): properties[p] = {"id": val.id, "unresolved": True} elif isinstance(val, CaosDBPythonEntity): - properties[p] = val.serialize(without_metadata, visited) + properties[p] = val.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited) elif isinstance(val, list): serializedelements = [] for element in val: if isinstance(element, CaosDBPythonUnresolvedReference): - elm = dict() + elm = {} elm["id"] = element.id elm["unresolved"] = True serializedelements.append(elm) elif isinstance(element, CaosDBPythonEntity): serializedelements.append( - element.serialize(without_metadata, - visited)) + element.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited)) else: serializedelements.append(element) properties[p] = serializedelements else: properties[p] = val - fulldict["properties"] = properties - fulldict["parents"] = parents - - if not without_metadata: - fulldict["metadata"] = metadata + if plain_json: + fulldict["id"] = getattr(self, "id") + fulldict["name"] = getattr(self, "name") + fulldict.update(properties) + else: + fulldict["properties"] = properties + fulldict["parents"] = parents + if not without_metadata: + fulldict["metadata"] = metadata return fulldict def __str__(self): -- GitLab From da7a10d5124a02fee57ee86cef9fac4db626206f Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Fri, 3 May 2024 13:16:55 +0200 Subject: [PATCH 04/10] MAINT: Formatting. --- src/linkahead/high_level_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py index 5b1682f3..cdbd81e3 100644 --- a/src/linkahead/high_level_api.py +++ b/src/linkahead/high_level_api.py @@ -44,7 +44,8 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, REFERENCE, TEXT, get_list_datatype, is_list_datatype, is_reference) -warnings.warn("""EXPERIMENTAL! The high_level_api module is experimental and may be changed or +warnings.warn(""" +EXPERIMENTAL! The high_level_api module is experimental and may be changed or removed in the future. Its purpose is to give an impression on how the Python client user interface might be changed.""") -- GitLab From fd0650aef3de47be12112937a09ca0380c8c1bb2 Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Wed, 22 May 2024 14:00:29 +0200 Subject: [PATCH 05/10] FIX: No infinite recursion for 1-character strings. --- src/linkahead/common/models.py | 4 ++-- unittests/test_issues.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 483c5231..9df9788e 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -1709,8 +1709,8 @@ def _parse_value(datatype, value): return ret # This is for a special case, where the xml parser could not differentiate - # between single values and lists with one element. As - if hasattr(value, "__len__") and len(value) == 1: + # between single values and lists with one element. + if hasattr(value, "__len__") and not isinstance(value, str) and len(value) == 1: return _parse_value(datatype, value[0]) # deal with references diff --git a/unittests/test_issues.py b/unittests/test_issues.py index 7472f710..ba934009 100644 --- a/unittests/test_issues.py +++ b/unittests/test_issues.py @@ -64,3 +64,9 @@ def test_issue_156(): # </ParentList> assert value is project assert parents[0].name == "RTName" + + +def test_parse_datatype(): + """No infinite recursion.""" + from linkahead.common.models import _parse_value + assert 1 == _parse_value("labels0", "1") -- GitLab From b1a00123707c72e256f97fb7ca34aff7ee97401c Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Mon, 9 Dec 2024 16:59:32 +0100 Subject: [PATCH 06/10] WIP: Better diff in case of different types. --- src/linkahead/apiutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index 1aa127d3..b0b65d8d 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -291,6 +291,7 @@ def compare_entities(entity0: Optional[Entity] = None, if entity0 is entity1: return diff + # FIXME Why not simply return a diff which says that the types are different?g if type(entity0) is not type(entity1): raise ValueError( "Comparison of different Entity types is not supported.") -- GitLab From b70d0f98bce6f7accfaffb9a2f159605d5d710df Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Mon, 9 Dec 2024 17:04:46 +0100 Subject: [PATCH 07/10] WIP: Better diff in case of different types. --- src/linkahead/apiutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index b0b65d8d..53336a4a 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -291,10 +291,10 @@ def compare_entities(entity0: Optional[Entity] = None, if entity0 is entity1: return diff - # FIXME Why not simply return a diff which says that the types are different?g + # FIXME Why not simply return a diff which says that the types are different? if type(entity0) is not type(entity1): - raise ValueError( - "Comparison of different Entity types is not supported.") + diff[0]["type"] = type(entity0) + diff[1]["type"] = type(entity1) # compare special attributes for attr in SPECIAL_ATTRIBUTES: -- GitLab From 49382a8fa5f0be460fd3fe34b7718021d25cf336 Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Tue, 8 Apr 2025 11:13:20 +0200 Subject: [PATCH 08/10] STYLE: Whitespace only. --- src/linkahead/high_level_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py index 44686a42..a835fbb3 100644 --- a/src/linkahead/high_level_api.py +++ b/src/linkahead/high_level_api.py @@ -688,9 +688,11 @@ class CaosDBPythonEntity(object): Parameters ---------- + without_metadata: bool, optional If True don't set the metadata field in order to increase readability. Not recommended if deserialization is needed. + plain_json: bool, optional If True, serialize to a plain dict without any additional information besides the property values, name and id. This should conform to the format as specified by the json schema generated by the -- GitLab From af843d65c6de11fea91f01728d2659c18d312b8f Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Tue, 8 Apr 2025 14:36:30 +0200 Subject: [PATCH 09/10] TEST: Added unit test for high level api serialization. --- unittests/test_high_level_api.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py index 82c1a5ca..e35dc678 100644 --- a/unittests/test_high_level_api.py +++ b/unittests/test_high_level_api.py @@ -322,6 +322,7 @@ def test_wrong_entity_for_file(): def test_serialization(): + # With ID r = db.Record(id=5, name="test", description="ok") r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx", importance="RECOMMENDED") @@ -333,6 +334,22 @@ def test_serialization(): for teststr in teststrs: assert teststr in text + serialized = convert_to_python_object(r).serialize() + assert serialized == {'role': 'Record', + 'name': 'test', + 'id': 5, + 'description': 'ok', + 'properties': {'v': 15}, + 'parents': [], + 'metadata': {'v': {'unit': 'kpx', + 'datatype': 'INTEGER', + 'importance': 'RECOMMENDED'}}} + + serialized_plain = convert_to_python_object(r).serialize(plain_json=True) + assert serialized_plain == {'id': 5, 'name': 'test', 'v': 15} + + # Without ID + r = db.Record(description="ok") r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx", importance="RECOMMENDED") @@ -341,6 +358,18 @@ def test_serialization(): assert "name" not in text assert "id" not in text + serialized = convert_to_python_object(r).serialize() + assert serialized == {'role': 'Record', + 'description': 'ok', + 'properties': {'v': 15}, + 'parents': [], + 'metadata': {'v': {'unit': 'kpx', + 'datatype': 'INTEGER', + 'importance': 'RECOMMENDED'}}} + + serialized_plain = convert_to_python_object(r).serialize(plain_json=True) + assert serialized_plain == {'id': None, 'name': None, 'v': 15} + def test_files(): # empty file: -- GitLab From 70588ba60165370b1262581b1fc620e3bbde3d94 Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Tue, 8 Apr 2025 14:40:34 +0200 Subject: [PATCH 10/10] DOCS: More documentation for the high level api serialization. --- CHANGELOG.md | 2 ++ src/linkahead/high_level_api.py | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f2498a..33142766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## ### Added ### + - convenience functions `value_matches_versionid`, `get_id_from_versionid` and `get_versionid` +- Parameter for high level API serialization to output a plain JSON. ### Changed ### diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py index a835fbb3..a0137dac 100644 --- a/src/linkahead/high_level_api.py +++ b/src/linkahead/high_level_api.py @@ -683,20 +683,28 @@ class CaosDBPythonEntity(object): return entity def serialize(self, without_metadata: bool = None, plain_json: bool = False, - visited: dict = None): + visited: dict = None) -> dict: """Serialize necessary information into a dict. -Parameters ----------- + Parameters + ---------- -without_metadata: bool, optional - If True don't set the metadata field in order to increase - readability. Not recommended if deserialization is needed. + without_metadata: bool, optional + If True don't set the metadata field in order to increase + readability. Not recommended if deserialization is needed. -plain_json: bool, optional - If True, serialize to a plain dict without any additional information besides the property values, - name and id. This should conform to the format as specified by the json schema generated by the - advanced user tools. This implies ``without_metadata = True``. + plain_json: bool, optional + If True, serialize to a plain dict without any additional information besides the property values, + name and id. This should conform to the format as specified by the json schema generated by the + advanced user tools. It also sets all properties as top level items of the resulting dict. This + implies ``without_metadata = True + + Returns + ------- + + out: dict + A dict corresponding to this entity. + ``. """ if plain_json: if without_metadata is None: -- GitLab