From 84d94c3e9cc7215c913f9b5a8ed684583aa86724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:12:21 +0100 Subject: [PATCH 1/3] Align wtih new idgen_random predicate --- terminusdb_client/tests/test_woqlQuery.py | 10 ++++---- .../tests/woqljson/woqlIdgenJson.py | 10 +++----- terminusdb_client/woqlquery/woql_query.py | 23 ++++++++++--------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/terminusdb_client/tests/test_woqlQuery.py b/terminusdb_client/tests/test_woqlQuery.py index 1e973f9e..d281b130 100644 --- a/terminusdb_client/tests/test_woqlQuery.py +++ b/terminusdb_client/tests/test_woqlQuery.py @@ -15,7 +15,7 @@ from .woqljson.woqlConcatJson import WOQL_CONCAT_JSON from .woqljson.woqlIdgenJson import ( WOQL_IDGEN_JSON, - WOQL_RANDOM_IDGEN_JSON, + WOQL_RANDOM_KEY_JSON, WOQL_UNIQUE_JSON, ) from .woqljson.woqlJoinSplitJson import WOQL_JOIN_SPLIT_JSON @@ -214,11 +214,9 @@ def test_idgen_method(self): woql_object = WOQLQuery().idgen("Station", "v:Start_ID", "v:Start_Station_URL") assert woql_object.to_dict() == WOQL_IDGEN_JSON - def test_random_idgen_method(self): - woql_object = WOQLQuery().random_idgen( - "Station", "v:Start_ID", "v:Start_Station_URL" - ) - assert woql_object.to_dict() == WOQL_RANDOM_IDGEN_JSON + def test_idgen_random_method(self): + woql_object = WOQLQuery().idgen_random("Person/", "v:Person_ID") + assert woql_object.to_dict() == WOQL_RANDOM_KEY_JSON def test_typecast_method(self): woql_object = WOQLQuery().typecast( diff --git a/terminusdb_client/tests/woqljson/woqlIdgenJson.py b/terminusdb_client/tests/woqljson/woqlIdgenJson.py index e539b302..5a09ecb8 100644 --- a/terminusdb_client/tests/woqljson/woqlIdgenJson.py +++ b/terminusdb_client/tests/woqljson/woqlIdgenJson.py @@ -27,18 +27,14 @@ }, } -WOQL_RANDOM_IDGEN_JSON = { +WOQL_RANDOM_KEY_JSON = { "@type": "RandomKey", "base": { "@type": "DataValue", - "data": {"@type": "xsd:string", "@value": "Station"}, - }, - "key_list": { - "@type": "DataValue", - "variable": "Start_ID", + "data": {"@type": "xsd:string", "@value": "Person/"}, }, "uri": { "@type": "NodeValue", - "variable": "Start_Station_URL", + "variable": "Person_ID", }, } diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 35fbad21..d03e62fd 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2207,35 +2207,36 @@ def idgen(self, prefix, input_var_list, output_var): self._cursor["uri"] = self._clean_node_value(output_var) return self - def random_idgen(self, prefix, key_list, uri): - """Randomly generates an ID and appends to the end of the key_list. + def idgen_random(self, prefix, uri): + """Generates a unique ID with cryptographically secure random suffix. + + Uses base64 encoding to generate 16-character random IDs that are + guaranteed to be unique across executions. Matches the server's + idgen_random/2 predicate and maintains consistency with the JavaScript client. Parameters ---------- prefix : str - prefix for the id - key_list : str - variable to generate id for + A prefix for the IDs to be generated (e.g. "Person/") uri : str - the variable to hold the id + Variable name or output target for the generated ID Returns ------- WOQLQuery object - query object that can be chained and/or execute + query object that can be chained and/or executed Examples ------- - >>> WOQLQuery().random_idgen("https://base.url",["page","1"],"v:obj_id").execute(client) - {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['obj_id'], 'bindings': [{'obj_id': 'http://base.url_page_1_rv1mfa59ekisdutnxx6zdt2fkockgah'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} + >>> WOQLQuery().idgen_random("Person/", "v:person_id").execute(client) + {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['person_id'], 'bindings': [{'person_id': 'Person/aB3dEf9GhI2jK4lM'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} """ if prefix and prefix == "args": - return ["base", "key_list", "uri"] + return ["base", "uri"] if self._cursor.get("@type"): self._wrap_cursor_with_and() self._cursor["@type"] = "RandomKey" self._cursor["base"] = self._clean_data_value(prefix) - self._cursor["key_list"] = self._data_list(key_list) self._cursor["uri"] = self._clean_node_value(uri) return self From c1a59f2e0a7060e3b65a9358bbf757b3fdf27fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:38:28 +0100 Subject: [PATCH 2/3] Add tests and fix linting --- .../tests/integration_tests/test_conftest.py | 21 ++- terminusdb_client/tests/test_client_init.py | 10 +- .../tests/test_woql_idgen_random.py | 122 ++++++++++++++++++ 3 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 terminusdb_client/tests/test_woql_idgen_random.py diff --git a/terminusdb_client/tests/integration_tests/test_conftest.py b/terminusdb_client/tests/integration_tests/test_conftest.py index 9d8d785f..61f9e728 100644 --- a/terminusdb_client/tests/integration_tests/test_conftest.py +++ b/terminusdb_client/tests/integration_tests/test_conftest.py @@ -1,5 +1,4 @@ """Unit tests for conftest.py helper functions""" -import pytest from unittest.mock import patch, Mock import requests @@ -19,7 +18,7 @@ def test_local_server_running_200(self, mock_get): mock_response = Mock() mock_response.status_code = 200 mock_get.return_value = mock_response - + assert is_local_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6363", timeout=2) @@ -29,21 +28,21 @@ def test_local_server_running_404(self, mock_get): mock_response = Mock() mock_response.status_code = 404 mock_get.return_value = mock_response - + assert is_local_server_running() is True @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') def test_local_server_not_running_connection_error(self, mock_get): """Test local server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() - + assert is_local_server_running() is False @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') def test_local_server_not_running_timeout(self, mock_get): """Test local server detection returns False on timeout""" mock_get.side_effect = requests.exceptions.Timeout() - + assert is_local_server_running() is False @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') @@ -52,7 +51,7 @@ def test_docker_server_running_200(self, mock_get): mock_response = Mock() mock_response.status_code = 200 mock_get.return_value = mock_response - + assert is_docker_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6366", timeout=2) @@ -62,14 +61,14 @@ def test_docker_server_running_404(self, mock_get): mock_response = Mock() mock_response.status_code = 404 mock_get.return_value = mock_response - + assert is_docker_server_running() is True @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') def test_docker_server_not_running(self, mock_get): """Test Docker server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() - + assert is_docker_server_running() is False @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') @@ -78,7 +77,7 @@ def test_jwt_server_running_200(self, mock_get): mock_response = Mock() mock_response.status_code = 200 mock_get.return_value = mock_response - + assert is_jwt_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6367", timeout=2) @@ -88,12 +87,12 @@ def test_jwt_server_running_404(self, mock_get): mock_response = Mock() mock_response.status_code = 404 mock_get.return_value = mock_response - + assert is_jwt_server_running() is True @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') def test_jwt_server_not_running(self, mock_get): """Test JWT server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() - + assert is_jwt_server_running() is False diff --git a/terminusdb_client/tests/test_client_init.py b/terminusdb_client/tests/test_client_init.py index e6d8db5c..62e2af0a 100644 --- a/terminusdb_client/tests/test_client_init.py +++ b/terminusdb_client/tests/test_client_init.py @@ -1,7 +1,5 @@ """Unit tests for Client initialization""" -import pytest from terminusdb_client.client import Client -from terminusdb_client.errors import InterfaceError class TestClientInitialization: @@ -29,9 +27,9 @@ def test_client_copy(self): client1 = Client("http://localhost:6363") client1.team = "test_team" client1.db = "test_db" - + client2 = client1.copy() - + assert client2.server_url == client1.server_url assert client2.team == client1.team assert client2.db == client1.db @@ -42,9 +40,9 @@ def test_client_copy_modifications_independent(self): """Test modifications to copied client don't affect original""" client1 = Client("http://localhost:6363") client1.team = "team1" - + client2 = client1.copy() client2.team = "team2" - + assert client1.team == "team1" assert client2.team == "team2" diff --git a/terminusdb_client/tests/test_woql_idgen_random.py b/terminusdb_client/tests/test_woql_idgen_random.py new file mode 100644 index 00000000..bb11d169 --- /dev/null +++ b/terminusdb_client/tests/test_woql_idgen_random.py @@ -0,0 +1,122 @@ +"""Unit tests for idgen_random WOQL method""" +from terminusdb_client.woqlquery.woql_query import WOQLQuery + + +class TestIdgenRandom: + """Test idgen_random WOQL method""" + + def test_idgen_random_basic(self): + """Test basic idgen_random functionality""" + woql = WOQLQuery().idgen_random("Person/", "v:PersonID") + result = woql.to_dict() + + assert result["@type"] == "RandomKey" + assert result["base"]["@type"] == "DataValue" + assert result["base"]["data"]["@type"] == "xsd:string" + assert result["base"]["data"]["@value"] == "Person/" + assert result["uri"]["@type"] == "NodeValue" + assert result["uri"]["variable"] == "PersonID" + + def test_idgen_random_with_prefix(self): + """Test idgen_random with different prefix formats""" + woql = WOQLQuery().idgen_random("http://example.org/Person/", "v:ID") + result = woql.to_dict() + + assert result["base"]["data"]["@value"] == "http://example.org/Person/" + assert result["uri"]["variable"] == "ID" + + def test_idgen_random_chaining(self): + """Test idgen_random can be chained with other operations""" + woql = (WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID")) + + result = woql.to_dict() + assert result["@type"] == "And" + assert len(result["and"]) == 2 + assert result["and"][0]["@type"] == "Triple" + assert result["and"][1]["@type"] == "RandomKey" + + def test_idgen_random_multiple_calls(self): + """Test multiple idgen_random calls in same query""" + woql = (WOQLQuery() + .idgen_random("Person/", "v:PersonID") + .idgen_random("Order/", "v:OrderID")) + + result = woql.to_dict() + assert result["@type"] == "And" + assert result["and"][0]["@type"] == "RandomKey" + assert result["and"][0]["base"]["data"]["@value"] == "Person/" + assert result["and"][1]["@type"] == "RandomKey" + assert result["and"][1]["base"]["data"]["@value"] == "Order/" + + def test_idgen_random_args_parameter(self): + """Test idgen_random args parameter returns parameter list""" + result = WOQLQuery().idgen_random("args", "v:ID") + + assert result == ["base", "uri"] + + def test_idgen_random_empty_prefix(self): + """Test idgen_random with empty prefix""" + woql = WOQLQuery().idgen_random("", "v:ID") + result = woql.to_dict() + + assert result["@type"] == "RandomKey" + assert result["base"]["data"]["@value"] == "" + + def test_idgen_random_variable_output(self): + """Test idgen_random output variable format""" + woql = WOQLQuery().idgen_random("Test/", "v:MyVar") + result = woql.to_dict() + + assert result["uri"]["variable"] == "MyVar" + + def test_idgen_random_in_query_chain(self): + """Test idgen_random in complex query chain""" + woql = (WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID") + .triple("v:PersonID", "@schema:name", "v:Name")) + + result = woql.to_dict() + assert result["@type"] == "And" + # WOQLQuery chains create nested And structures + # Verify RandomKey is present in the chain + has_random_key = False + + def check_for_random_key(obj): + nonlocal has_random_key + if isinstance(obj, dict): + if obj.get("@type") == "RandomKey": + has_random_key = True + for value in obj.values(): + check_for_random_key(value) + elif isinstance(obj, list): + for item in obj: + check_for_random_key(item) + + check_for_random_key(result) + assert has_random_key, "RandomKey should be present in query chain" + + def test_idgen_random_matches_expected_json(self): + """Test idgen_random produces expected WOQL JSON structure""" + from terminusdb_client.tests.woqljson.woqlIdgenJson import WOQL_RANDOM_KEY_JSON + + woql = WOQLQuery().idgen_random("Person/", "v:Person_ID") + result = woql.to_dict() + + assert result == WOQL_RANDOM_KEY_JSON + + def test_idgen_random_with_special_characters_in_prefix(self): + """Test idgen_random handles special characters in prefix""" + woql = WOQLQuery().idgen_random("Test/2024-11/", "v:ID") + result = woql.to_dict() + + assert result["base"]["data"]["@value"] == "Test/2024-11/" + + def test_idgen_random_preserves_cursor_state(self): + """Test idgen_random returns self for method chaining""" + woql = WOQLQuery() + result = woql.idgen_random("Test/", "v:ID") + + assert result is woql # Should return same object for chaining From 586aefad867f3c832304817c00932885b8e50ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 10:08:56 +0100 Subject: [PATCH 3/3] Add alias for random_idgen --- terminusdb_client/woqlquery/woql_query.py | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index d03e62fd..d54ea363 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2240,6 +2240,32 @@ def idgen_random(self, prefix, uri): self._cursor["uri"] = self._clean_node_value(uri) return self + def random_idgen(self, prefix, uri): + """Generates a unique ID with cryptographically secure random suffix (alias for idgen_random). + + This is an alias for idgen_random() for compatibility with older code. + Uses base64 encoding to generate 16-character random IDs that are + guaranteed to be unique across executions. + + Parameters + ---------- + prefix : str + A prefix for the IDs to be generated (e.g. "Person/") + uri : str + Variable name or output target for the generated ID + + Returns + ------- + WOQLQuery object + query object that can be chained and/or executed + + Examples + ------- + >>> WOQLQuery().random_idgen("Person/", "v:person_id").execute(client) + {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['person_id'], 'bindings': [{'person_id': 'Person/aB3dEf9GhI2jK4lM'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} + """ + return self.idgen_random(prefix, uri) + def upper(self, left, right): """Changes a string to upper-case - input is in left, output in right