From 5268e1180b46c56243efff48112b1973b0a3f630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 4 Jul 2026 16:39:28 +0200 Subject: [PATCH 1/3] Add unfold option to diff endpoint --- terminusdb_client/client/Client.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/terminusdb_client/client/Client.py b/terminusdb_client/client/Client.py index 72c5d8fc..1e9c0fb2 100644 --- a/terminusdb_client/client/Client.py +++ b/terminusdb_client/client/Client.py @@ -2379,7 +2379,9 @@ def diff_object(self, before_object, after_object): ) ) - def diff_version(self, before_version, after_version): + def diff_version( + self, before_version, after_version, unfold: bool = True + ): """Diff two different versions. Can either be a branch or a commit Parameters @@ -2388,6 +2390,10 @@ def diff_version(self, before_version, after_version): Commit or branch of the before version to compare after_version : string Commit or branch of the after version to compare + unfold : bool, optional + Expand subdocuments inline when retrieving documents from the + specified versions. Defaults to ``True``; set to ``False`` to + compare folded documents by reference. """ self._check_connection(check_db=False) return json.loads( @@ -2398,6 +2404,7 @@ def diff_version(self, before_version, after_version): json={ "before_data_version": before_version, "after_data_version": after_version, + "unfold": unfold, }, auth=self._auth(), ) @@ -2423,6 +2430,7 @@ def diff( List["DocumentTemplate"], # noqa:F821 ], document_id: Union[str, None] = None, + unfold: bool = True, ): """DEPRECATED @@ -2430,6 +2438,13 @@ def diff( Do not connect when using public API. + Parameters + ---------- + unfold : bool, optional + Expand subdocuments inline when retrieving documents from commits + or branches. Only used when ``before`` or ``after`` is a version + reference. Defaults to ``True``. + Returns ------- obj @@ -2461,6 +2476,9 @@ def diff( raise ValueError( "`document_id` can only be used in conjusction with a data version or commit ID as `before`, not a document object" ) + if isinstance(before, str) or isinstance(after, str): + request_dict["unfold"] = unfold + if self._connected: result = _finish_response( self._session.post( From ebce2a9fe9354fc7ed3f5365c4ab2bce221b1508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 4 Jul 2026 16:34:31 +0200 Subject: [PATCH 2/3] Add extra options and bump to version 12.0.7 --- .github/insert_docs.py | 2 +- terminusdb_client/__version__.py | 2 +- terminusdb_client/client/Client.py | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/insert_docs.py b/.github/insert_docs.py index 413e1f37..fcca7c55 100644 --- a/.github/insert_docs.py +++ b/.github/insert_docs.py @@ -8,7 +8,7 @@ def parameter_to_document(param): return {'@type': 'Parameter', 'type': param.type, 'name': param.name, 'summary': "\n".join(param.desc)} -classes_to_scan = ['Client']#, 'WOQLQuery'] +classes_to_scan = ['Client', 'WOQLQuery'] classes = [] for name, obj in inspect.getmembers(terminusdb_client): diff --git a/terminusdb_client/__version__.py b/terminusdb_client/__version__.py index b9c4f68a..c42dda10 100644 --- a/terminusdb_client/__version__.py +++ b/terminusdb_client/__version__.py @@ -1,7 +1,7 @@ __title__ = "terminusdb-client" __description__ = "TerminusDB client library for accessing the Terminus DB API" __url__ = "" -__version__ = "11.1.0" +__version__ = "12.0.5" __build__ = 00 __author__ = "TerminusDB group" __author_email__ = "team@terminusdb.com" diff --git a/terminusdb_client/client/Client.py b/terminusdb_client/client/Client.py index 1e9c0fb2..0281b072 100644 --- a/terminusdb_client/client/Client.py +++ b/terminusdb_client/client/Client.py @@ -1480,6 +1480,8 @@ def insert_document( last_data_version: Optional[str] = None, compress: Union[str, int] = 1024, raw_json: bool = False, + merge_repeats: bool = False, + overwrite: bool = False, ) -> None: """Inserts the specified document(s) @@ -1499,6 +1501,10 @@ def insert_document( If it is an integer, size of the data larger than this (in bytes) will be compress with gzip in the request (assume encoding as UTF-8, 0 = always compress). If it is `never` it will never compress the data. raw_json : bool Update as raw json + merge_repeats : bool + If True, duplicate document IDs in the same request are silently accepted instead of causing an error. + overwrite : bool + If True, existing documents with the same ID will be overwritten instead of causing an error. This enables upsert behavior. Raises ------ @@ -1518,6 +1524,8 @@ def insert_document( else: params["full_replace"] = "false" params["raw_json"] = "true" if raw_json else "false" + params["merge_repeats"] = "true" if merge_repeats else "false" + params["overwrite"] = "true" if overwrite else "false" headers = self._default_headers.copy() if last_data_version is not None: @@ -1587,6 +1595,7 @@ def replace_document( compress: Union[str, int] = 1024, create: bool = False, raw_json: bool = False, + merge_repeats: bool = False, ) -> dict: """Updates the specified document(s) @@ -1606,6 +1615,8 @@ def replace_document( Create the document if it does not yet exist. raw_json : bool Update as raw json + merge_repeats : bool + If True, duplicate document IDs in the same request are silently accepted instead of causing an error. Raises ------ @@ -1617,6 +1628,7 @@ def replace_document( params["graph_type"] = graph_type params["create"] = "true" if create else "false" params["raw_json"] = "true" if raw_json else "false" + params["merge_repeats"] = "true" if merge_repeats else "false" headers = self._default_headers.copy() if last_data_version is not None: @@ -1667,6 +1679,7 @@ def update_document( commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, compress: Union[str, int] = 1024, + merge_repeats: bool = False, ) -> None: """Updates the specified document(s). Add the document if not existed. @@ -1682,6 +1695,8 @@ def update_document( Last version before the update, used to check if the document has been changed unknowingly compress : str or int If it is an integer, size of the data larger than this (in bytes) will be compress with gzip in the request (assume encoding as UTF-8, 0 = always compress). If it is `never` it will never compress the data. + merge_repeats : bool + If True, duplicate document IDs in the same request are silently accepted instead of causing an error. Raises ------ @@ -1689,7 +1704,8 @@ def update_document( if the client does not connect to a database """ self.replace_document( - document, graph_type, commit_msg, last_data_version, compress, True + document, graph_type, commit_msg, last_data_version, compress, True, + merge_repeats=merge_repeats, ) def delete_document( From 18527521694e577a864cae19ed154b0d81b9c1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 31 Oct 2026 02:43:56 +0100 Subject: [PATCH 3/3] Improve coverage --- terminusdb_client/tests/test_Client.py | 179 +++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/terminusdb_client/tests/test_Client.py b/terminusdb_client/tests/test_Client.py index dd56e920..d24bb870 100644 --- a/terminusdb_client/tests/test_Client.py +++ b/terminusdb_client/tests/test_Client.py @@ -745,3 +745,182 @@ def test_convert_document_keeps_prefixed_and_schema_ids(): [{"@id": "Coordinate", "@type": "Class"}], "schema" ) assert docs[0]["@id"] == "Coordinate" + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_version(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + + client.diff_version("main", "changes") + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after_data_version": "changes", + "unfold": True, + }, + ) + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_version_folded(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + + client.diff_version("main", "changes", unfold=False) + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after_data_version": "changes", + "unfold": False, + }, + ) + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_versions(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + + result = client.diff("main", "changes") + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after_data_version": "changes", + "unfold": True, + }, + ) + assert result.content is not None + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_versions_folded(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + + client.diff("main", "changes", unfold=False) + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after_data_version": "changes", + "unfold": False, + }, + ) + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_version_and_document(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + janine = {"@id": "Person/Janine", "@type": "Person", "name": "Janine"} + + client.diff("main", janine) + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after": janine, + "unfold": True, + }, + ) + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_documents_no_unfold(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + jane = {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} + janine = {"@id": "Person/Jane", "@type": "Person", "name": "Janine"} + + client.diff(jane, janine) + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={"before": jane, "after": janine}, + ) + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +def test_diff_document_id(mocked_post, mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + janine = {"@id": "Person/Jane", "@type": "Person", "name": "Janine"} + + client.diff("commit123", janine, document_id="Person/Jane") + + client._session.post.assert_called_once_with( + "http://localhost:6363/api/diff/admin/myDBName/local/branch/main", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "commit123", + "after": janine, + "document_id": "terminusdb:///data/Person/Jane", + "unfold": True, + }, + ) + + +@mock.patch.object(requests, "post", side_effect=mocked_request_success) +def test_diff_no_connection(mocked_post): + client = Client("http://localhost:6363") + + result = client.diff("main", "changes") + + mocked_post.assert_called_once_with( + "http://localhost:6363", + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + json={ + "before_data_version": "main", + "after_data_version": "changes", + "unfold": True, + }, + ) + assert result.content is not None + + +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +def test_diff_document_id_invalid(mocked_get, mocked_head): + client = Client("http://localhost:6363", user="admin", key="root", team="admin") + client.connect(db="myDBName") + jane = {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} + janine = {"@id": "Person/Jane", "@type": "Person", "name": "Janine"} + + with pytest.raises(ValueError): + client.diff(jane, janine, document_id="Person/Jane")