From e77fe02bc25787313a03daf6b542a8b0db9b71dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 05:51:28 +0100 Subject: [PATCH 01/14] Make tests run again. Remove cloud dependency. --- .../tests/integration_tests/conftest.py | 75 +++++++++++++++++-- .../test-docker-compose-jwt.yml | 2 - .../integration_tests/test-docker-compose.yml | 2 - .../tests/integration_tests/test_client.py | 28 ++----- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/terminusdb_client/tests/integration_tests/conftest.py b/terminusdb_client/tests/integration_tests/conftest.py index b12e11a0..5258d04c 100644 --- a/terminusdb_client/tests/integration_tests/conftest.py +++ b/terminusdb_client/tests/integration_tests/conftest.py @@ -5,7 +5,37 @@ import pytest import requests -MAX_CONTAINER_STARTUP_TIME = 30 +MAX_CONTAINER_STARTUP_TIME = 120 # Increased from 30 to 120 seconds for slower systems + +# Check if a local TerminusDB test server is already running +def is_local_server_running(): + """Check if local TerminusDB server is running at http://127.0.0.1:6363""" + try: + response = requests.get("http://127.0.0.1:6363", timeout=2) + # Server responds with 404 for root path, which means it's running + return response.status_code in [200, 404] + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + return False + + +def is_docker_server_running(): + """Check if Docker TerminusDB server is already running at http://127.0.0.1:6366""" + try: + response = requests.get("http://127.0.0.1:6366", timeout=2) + # Server responds with 404 for root path, which means it's running + return response.status_code in [200, 404] + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + return False + + +def is_jwt_server_running(): + """Check if JWT Docker TerminusDB server is already running at http://127.0.0.1:6367""" + try: + response = requests.get("http://127.0.0.1:6367", timeout=2) + # Server responds with 404 for root path, which means it's running + return response.status_code in [200, 404] + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + return False def is_docker_installed(): @@ -39,6 +69,13 @@ def docker_url_jwt(pytestconfig): # we are using subprocess in case we need to access some of the outputs # most likely jwt_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3RrZXkifQ.eyJodHRwOi8vdGVybWludXNkYi5jb20vc2NoZW1hL3N5c3RlbSNhZ2VudF9uYW1lIjoiYWRtaW4iLCJodHRwOi8vdGVybWludXNkYi5jb20vc2NoZW1hL3N5c3RlbSN1c2VyX2lkZW50aWZpZXIiOiJhZG1pbkB1c2VyLmNvbSIsImlzcyI6Imh0dHBzOi8vdGVybWludXNodWIuZXUuYXV0aDAuY29tLyIsInN1YiI6ImFkbWluIiwiYXVkIjpbImh0dHBzOi8vdGVybWludXNodWIvcmVnaXN0ZXJVc2VyIiwiaHR0cHM6Ly90ZXJtaW51c2h1Yi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNTkzNzY5MTgzLCJhenAiOiJNSkpuZEdwMHpVZE03bzNQT1RRUG1SSkltWTJobzBhaSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwifQ.Ru03Bi6vSIQ57bC41n6fClSdxlb61m0xX6Q34Yh91gql0_CyfYRWTuqzqPMFoCefe53hPC5E-eoSFdID_u6w1ih_pH-lTTqus9OWgi07Qou3QNs8UZBLiM4pgLqcBKs0N058jfg4y6h9GjIBGVhX9Ni2ez3JGNcz1_U45BhnreE" + + # Check if JWT server is already running (port 6367) + if is_jwt_server_running(): + print("\n✓ Using existing JWT Docker TerminusDB server at http://127.0.0.1:6367") + yield ("http://127.0.0.1:6367", jwt_token) + return # Don't clean up - server was already running + pytestconfig.getoption("docker_compose") output = subprocess.run( [ @@ -77,7 +114,8 @@ def docker_url_jwt(pytestconfig): if service.stdout == b"terminusdb-server\n": try: response = requests.get(test_url) - assert response.status_code == 200 + # Server responds with 404 for root path, which means it's running + assert response.status_code in [200, 404] break except (requests.exceptions.ConnectionError, AssertionError): pass @@ -87,7 +125,7 @@ def docker_url_jwt(pytestconfig): if seconds_waited > MAX_CONTAINER_STARTUP_TIME: clean_up_container() - raise RuntimeError("Container was to slow to startup") + raise RuntimeError(f"JWT Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)") yield (test_url, jwt_token) clean_up_container() @@ -95,8 +133,29 @@ def docker_url_jwt(pytestconfig): @pytest.fixture(scope="module") def docker_url(pytestconfig): - # we are using subprocess in case we need to access some of the outputs - # most likely + """ + Provides a TerminusDB server URL for integration tests. + Prefers local test server if running, otherwise starts Docker container. + + NOTE: This fixture returns just the URL. Tests expect AUTOLOGIN mode (no authentication). + If using local server with authentication, use TERMINUSDB_AUTOLOGIN=true when starting it. + """ + # Check if local test server is already running (port 6363) + if is_local_server_running(): + print("\n✓ Using existing local TerminusDB test server at http://127.0.0.1:6363") + print("⚠️ WARNING: Local server should be started with TERMINUSDB_AUTOLOGIN=true") + print(" Or use: TERMINUSDB_SERVER_AUTOLOGIN=true ./tests/terminusdb-test-server.sh restart") + yield "http://127.0.0.1:6363" + return # Don't clean up - server was already running + + # Check if Docker container is already running (port 6366) + if is_docker_server_running(): + print("\n✓ Using existing Docker TerminusDB server at http://127.0.0.1:6366") + yield "http://127.0.0.1:6366" + return # Don't clean up - server was already running + + # No server found, start Docker container + print("\n⚠ No server found, starting Docker container with AUTOLOGIN...") pytestconfig.getoption("docker_compose") output = subprocess.run( [ @@ -134,7 +193,9 @@ def docker_url(pytestconfig): if service.stdout == b"terminusdb-server\n": try: response = requests.get(test_url) - assert response.status_code == 200 + # Server responds with 404 for root path, which means it's running + assert response.status_code in [200, 404] + print(f"✓ Docker container started successfully after {seconds_waited}s") break except (requests.exceptions.ConnectionError, AssertionError): pass @@ -144,7 +205,7 @@ def docker_url(pytestconfig): if seconds_waited > MAX_CONTAINER_STARTUP_TIME: clean_up_container() - raise RuntimeError("Container was to slow to startup") + raise RuntimeError(f"Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)") yield test_url clean_up_container() diff --git a/terminusdb_client/tests/integration_tests/test-docker-compose-jwt.yml b/terminusdb_client/tests/integration_tests/test-docker-compose-jwt.yml index 9d78b4c9..a5842fb1 100644 --- a/terminusdb_client/tests/integration_tests/test-docker-compose-jwt.yml +++ b/terminusdb_client/tests/integration_tests/test-docker-compose-jwt.yml @@ -1,5 +1,3 @@ -version: "3" - volumes: terminusdb_storage: diff --git a/terminusdb_client/tests/integration_tests/test-docker-compose.yml b/terminusdb_client/tests/integration_tests/test-docker-compose.yml index cf1518d0..868a28b9 100644 --- a/terminusdb_client/tests/integration_tests/test-docker-compose.yml +++ b/terminusdb_client/tests/integration_tests/test-docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - volumes: terminusdb_storage: diff --git a/terminusdb_client/tests/integration_tests/test_client.py b/terminusdb_client/tests/integration_tests/test_client.py index 00661bcb..7d3f0cbc 100644 --- a/terminusdb_client/tests/integration_tests/test_client.py +++ b/terminusdb_client/tests/integration_tests/test_client.py @@ -414,12 +414,8 @@ def test_diff_ops(docker_url, test_schema): client = Client(docker_url, user_agent=test_user_agent) client.connect(user="admin", team="admin") client.create_database("test_diff_ops") - public_diff = Client( - "https://cloud.terminusdb.com/jsondiff", user_agent=test_user_agent - ) - public_patch = Client( - "https://cloud.terminusdb.com/jsonpatch", user_agent=test_user_agent - ) + # NOTE: Public API endpoints (jsondiff/jsonpatch) no longer exist + # Testing authenticated diff/patch only result_patch = Patch( json='{"@id": "Person/Jane", "name" : { "@op" : "SwapValue", "@before" : "Jane", "@after": "Janine" }}' ) @@ -427,12 +423,7 @@ def test_diff_ops(docker_url, test_schema): {"@id": "Person/Jane", "@type": "Person", "name": "Jane"}, {"@id": "Person/Jane", "@type": "Person", "name": "Janine"}, ) - public_result = public_diff.diff( - {"@id": "Person/Jane", "@type": "Person", "name": "Jane"}, - {"@id": "Person/Jane", "@type": "Person", "name": "Janine"}, - ) assert result.content == result_patch.content - assert public_result.content == result_patch.content Person = test_schema.object.get("Person") jane = Person( @@ -446,7 +437,6 @@ def test_diff_ops(docker_url, test_schema): age=18, ) result = client.diff(jane, janine) - public_result = public_diff.diff(jane, janine) # test commit_id and data_version with after obj test_schema.commit(client) jane_id = client.insert_document(jane)[0] @@ -466,7 +456,6 @@ def test_diff_ops(docker_url, test_schema): commit_id_result_all = client.diff(current_commit, new_commit) data_version_result_all = client.diff(data_version, new_data_version) assert result.content == result_patch.content - assert public_result.content == result_patch.content assert commit_id_result.content == result_patch.content assert commit_id_result2.content == result_patch.content assert data_version_result.content == result_patch.content @@ -476,21 +465,12 @@ def test_diff_ops(docker_url, test_schema): assert client.patch( {"@id": "Person/Jane", "@type": "Person", "name": "Jane"}, result_patch ) == {"@id": "Person/Jane", "@type": "Person", "name": "Janine"} - assert public_patch.patch( - {"@id": "Person/Jane", "@type": "Person", "name": "Jane"}, result_patch - ) == {"@id": "Person/Jane", "@type": "Person", "name": "Janine"} assert client.patch(jane, result_patch) == { "@id": "Person/Jane", "@type": "Person", "name": "Janine", "age": 18, } - assert public_patch.patch(jane, result_patch) == { - "@id": "Person/Jane", - "@type": "Person", - "name": "Janine", - "age": 18, - } my_schema = test_schema.copy() my_schema.object.pop("Employee") assert my_schema.to_dict() != test_schema.to_dict() @@ -545,6 +525,10 @@ def test_diff_ops_no_auth(test_schema, terminusx_token): # assert client.patch(test_schema, result) == my_schema.to_dict() +@pytest.mark.skipif( + os.environ.get("TERMINUSDB_TEST_JWT") is None, + reason="JWT testing not enabled. Set TERMINUSDB_TEST_JWT=1 to enable JWT tests." +) def test_jwt(docker_url_jwt): # create client url = docker_url_jwt[0] From 44e663bb148732d48e806faaac0ed80a0b6696d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:08:57 +0100 Subject: [PATCH 02/14] Get python client up to date --- terminusdb_client/tests/integration_tests/test_schema.py | 4 ++-- terminusdb_client/tests/test_Client.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/terminusdb_client/tests/integration_tests/test_schema.py b/terminusdb_client/tests/integration_tests/test_schema.py index 55988686..63cfe6d1 100644 --- a/terminusdb_client/tests/integration_tests/test_schema.py +++ b/terminusdb_client/tests/integration_tests/test_schema.py @@ -103,7 +103,7 @@ def test_insert_cheuk(docker_url, test_schema): assert item["address_of"]["postal_code"] == "A12 345" assert item["address_of"]["street"] == "123 Abc Street" assert item["name"] == "Cheuk" - assert item["age"] == "21" + assert item["age"] == 21 assert item["contact_number"] == "07777123456" assert item["managed_by"] == item["@id"] else: @@ -185,7 +185,7 @@ def test_insert_cheuk_again(docker_url, test_schema): assert item["address_of"]["postal_code"] == "A12 345" assert item["address_of"]["street"] == "123 Abc Street" assert item["name"] == "Cheuk" - assert item["age"] == "21" + assert item["age"] == 21 assert item["contact_number"] == "07777123456" assert item["managed_by"] == item["@id"] elif item.get("@type") == "Coordinate": diff --git a/terminusdb_client/tests/test_Client.py b/terminusdb_client/tests/test_Client.py index d8d4862b..fadd11dd 100644 --- a/terminusdb_client/tests/test_Client.py +++ b/terminusdb_client/tests/test_Client.py @@ -177,6 +177,7 @@ def test_has_database(mocked_requests, mocked_requests2): f"http://localhost:6363/api/db/admin/{db_name}", auth=("admin", "root"), headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + allow_redirects=True, ) From 279c5d50c9cbebcb06dced6113fbb6679568359e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:19:54 +0100 Subject: [PATCH 03/14] Fix linting issues --- terminusdb_client/tests/integration_tests/conftest.py | 11 ++++++----- tox.ini | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/terminusdb_client/tests/integration_tests/conftest.py b/terminusdb_client/tests/integration_tests/conftest.py index d5c60bcb..c5e5d6e6 100644 --- a/terminusdb_client/tests/integration_tests/conftest.py +++ b/terminusdb_client/tests/integration_tests/conftest.py @@ -7,6 +7,7 @@ MAX_CONTAINER_STARTUP_TIME = 120 # Increased from 30 to 120 seconds for slower systems + # Check if a local TerminusDB test server is already running def is_local_server_running(): """Check if local TerminusDB server is running at http://127.0.0.1:6363""" @@ -69,13 +70,13 @@ def docker_url_jwt(pytestconfig): # we are using subprocess in case we need to access some of the outputs # most likely jwt_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3RrZXkifQ.eyJodHRwOi8vdGVybWludXNkYi5jb20vc2NoZW1hL3N5c3RlbSNhZ2VudF9uYW1lIjoiYWRtaW4iLCJodHRwOi8vdGVybWludXNkYi5jb20vc2NoZW1hL3N5c3RlbSN1c2VyX2lkZW50aWZpZXIiOiJhZG1pbkB1c2VyLmNvbSIsImlzcyI6Imh0dHBzOi8vdGVybWludXNodWIuZXUuYXV0aDAuY29tLyIsInN1YiI6ImFkbWluIiwiYXVkIjpbImh0dHBzOi8vdGVybWludXNodWIvcmVnaXN0ZXJVc2VyIiwiaHR0cHM6Ly90ZXJtaW51c2h1Yi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNTkzNzY5MTgzLCJhenAiOiJNSkpuZEdwMHpVZE03bzNQT1RRUG1SSkltWTJobzBhaSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwifQ.Ru03Bi6vSIQ57bC41n6fClSdxlb61m0xX6Q34Yh91gql0_CyfYRWTuqzqPMFoCefe53hPC5E-eoSFdID_u6w1ih_pH-lTTqus9OWgi07Qou3QNs8UZBLiM4pgLqcBKs0N058jfg4y6h9GjIBGVhX9Ni2ez3JGNcz1_U45BhnreE" - + # Check if JWT server is already running (port 6367) if is_jwt_server_running(): print("\n✓ Using existing JWT Docker TerminusDB server at http://127.0.0.1:6367") yield ("http://127.0.0.1:6367", jwt_token) return # Don't clean up - server was already running - + pytestconfig.getoption("docker_compose") output = subprocess.run( [ @@ -138,7 +139,7 @@ def docker_url(pytestconfig): """ Provides a TerminusDB server URL for integration tests. Prefers local test server if running, otherwise starts Docker container. - + NOTE: This fixture returns just the URL. Tests expect AUTOLOGIN mode (no authentication). If using local server with authentication, use TERMINUSDB_AUTOLOGIN=true when starting it. """ @@ -149,13 +150,13 @@ def docker_url(pytestconfig): print(" Or use: TERMINUSDB_SERVER_AUTOLOGIN=true ./tests/terminusdb-test-server.sh restart") yield "http://127.0.0.1:6363" return # Don't clean up - server was already running - + # Check if Docker container is already running (port 6366) if is_docker_server_running(): print("\n✓ Using existing Docker TerminusDB server at http://127.0.0.1:6366") yield "http://127.0.0.1:6366" return # Don't clean up - server was already running - + # No server found, start Docker container print("\n⚠ No server found, starting Docker container with AUTOLOGIN...") pytestconfig.getoption("docker_compose") diff --git a/tox.ini b/tox.ini index 5a94850b..8139d4a3 100644 --- a/tox.ini +++ b/tox.ini @@ -27,4 +27,4 @@ passenv = TERMINUSX_TOKEN [flake8] # S501 is turned off for now, check if the verify=false for request is ok ignore = E501,W503,S101,S310,A003,E203,S607,S603,S404,W391,N814,N804,S106,F722,S105,E266,S311,N806 -exclude = .*/,__pycache__,docs/,build/ +exclude = .*/,__pycache__,docs/,build/,venv/,.tox/ From 9dc24855f2e27627e11aca32f3ff5945532255ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:31:38 +0100 Subject: [PATCH 04/14] Add configuration testsing coverage --- .../tests/integration_tests/test_conftest.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 terminusdb_client/tests/integration_tests/test_conftest.py diff --git a/terminusdb_client/tests/integration_tests/test_conftest.py b/terminusdb_client/tests/integration_tests/test_conftest.py new file mode 100644 index 00000000..9d8d785f --- /dev/null +++ b/terminusdb_client/tests/integration_tests/test_conftest.py @@ -0,0 +1,99 @@ +"""Unit tests for conftest.py helper functions""" +import pytest +from unittest.mock import patch, Mock +import requests + +from .conftest import ( + is_local_server_running, + is_docker_server_running, + is_jwt_server_running, +) + + +class TestServerDetection: + """Test server detection helper functions""" + + @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + def test_local_server_running_200(self, mock_get): + """Test local server detection returns True for HTTP 200""" + 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) + + @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + def test_local_server_running_404(self, mock_get): + """Test local server detection returns True for HTTP 404""" + 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') + def test_docker_server_running_200(self, mock_get): + """Test Docker server detection returns True for HTTP 200""" + 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) + + @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + def test_docker_server_running_404(self, mock_get): + """Test Docker server detection returns True for HTTP 404""" + 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') + def test_jwt_server_running_200(self, mock_get): + """Test JWT server detection returns True for HTTP 200""" + 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) + + @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + def test_jwt_server_running_404(self, mock_get): + """Test JWT server detection returns True for HTTP 404""" + 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 From 0df5e9fe5fc8d68e1c19c364f6f1bc6bba37fcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:35:05 +0100 Subject: [PATCH 05/14] Improve test coverage --- terminusdb_client/tests/test_client_init.py | 50 +++++++++++++++++++++ terminusdb_client/tests/test_graphtype.py | 26 +++++++++++ 2 files changed, 76 insertions(+) create mode 100644 terminusdb_client/tests/test_client_init.py create mode 100644 terminusdb_client/tests/test_graphtype.py diff --git a/terminusdb_client/tests/test_client_init.py b/terminusdb_client/tests/test_client_init.py new file mode 100644 index 00000000..e6d8db5c --- /dev/null +++ b/terminusdb_client/tests/test_client_init.py @@ -0,0 +1,50 @@ +"""Unit tests for Client initialization""" +import pytest +from terminusdb_client.client import Client +from terminusdb_client.errors import InterfaceError + + +class TestClientInitialization: + """Test Client initialization and basic properties""" + + def test_client_init_basic(self): + """Test basic Client initialization""" + client = Client("http://localhost:6363") + assert client.server_url == "http://localhost:6363" + assert client._connected is False + + def test_client_init_with_user_agent(self): + """Test Client initialization with custom user agent""" + client = Client("http://localhost:6363", user_agent="test-agent/1.0") + assert "test-agent/1.0" in client._default_headers.get("user-agent", "") + + def test_client_init_with_trailing_slash(self): + """Test Client handles URLs with trailing slashes""" + client = Client("http://localhost:6363/") + # Should work without errors + assert client.server_url in ["http://localhost:6363", "http://localhost:6363/"] + + def test_client_copy(self): + """Test Client copy method creates independent instance""" + 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 + # Ensure it's a different object + assert client2 is not client1 + + 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_graphtype.py b/terminusdb_client/tests/test_graphtype.py new file mode 100644 index 00000000..a32347d4 --- /dev/null +++ b/terminusdb_client/tests/test_graphtype.py @@ -0,0 +1,26 @@ +"""Unit tests for GraphType enum""" +from terminusdb_client.client import GraphType + + +class TestGraphType: + """Test GraphType enum values""" + + def test_graphtype_instance(self): + """Test GraphType.INSTANCE value""" + assert GraphType.INSTANCE == "instance" + + def test_graphtype_schema(self): + """Test GraphType.SCHEMA value""" + assert GraphType.SCHEMA == "schema" + + def test_graphtype_enum_members(self): + """Test GraphType has exactly two members""" + members = list(GraphType) + assert len(members) == 2 + assert GraphType.INSTANCE in members + assert GraphType.SCHEMA in members + + def test_graphtype_values(self): + """Test all GraphType enum values exist""" + assert hasattr(GraphType, 'INSTANCE') + assert hasattr(GraphType, 'SCHEMA') From d72c1ab971597a4fea609627eb348e74776f4a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:39:56 +0100 Subject: [PATCH 06/14] Fix linting errors --- .../tests/integration_tests/test_conftest.py | 21 +++++++++---------- terminusdb_client/tests/test_client_init.py | 10 ++++----- 2 files changed, 14 insertions(+), 17 deletions(-) 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" From da9cfed32910b2fd4eb027a58162d1fd93be3e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:53:45 +0100 Subject: [PATCH 07/14] Add python 3.12 and fix inaccurate test --- .github/workflows/python.yml | 2 +- .../tests/integration_tests/test_client.py | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 3e291240..7b1173ba 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/terminusdb_client/tests/integration_tests/test_client.py b/terminusdb_client/tests/integration_tests/test_client.py index 7d3f0cbc..4b91a52a 100644 --- a/terminusdb_client/tests/integration_tests/test_client.py +++ b/terminusdb_client/tests/integration_tests/test_client.py @@ -332,6 +332,14 @@ def test_get_organization_user_databases(docker_url): client.create_organization(org_name) client.create_database(db_name, team=org_name) client.create_database(db_name2, team=org_name) + + # BEFORE grant: verify our specific databases are NOT accessible to admin user + databases_before = client.get_organization_user_databases(org=org_name, username="admin") + db_names_before = {db['name'] for db in databases_before} + assert db_name not in db_names_before, f"{db_name} should not be accessible before capability grant" + assert db_name2 not in db_names_before, f"{db_name2} should not be accessible before capability grant" + + # Grant capabilities to admin user for the organization capability_change = { "operation": "grant", "scope": f"Organization/{org_name}", @@ -341,10 +349,15 @@ def test_get_organization_user_databases(docker_url): ] } client.change_capabilities(capability_change) - databases = client.get_organization_user_databases(org=org_name, username="admin") - assert len(databases) == 2 - assert databases[0]['name'] == db_name - assert databases[1]['name'] == db_name2 + + # AFTER grant: verify our specific databases ARE accessible to admin user + databases_after = client.get_organization_user_databases(org=org_name, username="admin") + db_names_after = {db['name'] for db in databases_after} + # Check both our databases are now accessible (order not guaranteed) + assert db_name in db_names_after, f"{db_name} should be accessible after capability grant" + assert db_name2 in db_names_after, f"{db_name2} should be accessible after capability grant" + # Verify admin team database does NOT appear in organization results + assert db_name + "admin" not in db_names_after, f"{db_name}admin should not appear in {org_name} results" def test_has_database(docker_url): From 20bac2b28a26a015017a9816f94dcf04c3219faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 06:56:55 +0100 Subject: [PATCH 08/14] Add support for python 3.12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d56f8e7f..5b1b4d20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "Apache Software License" readme = "README.md" [tool.poetry.dependencies] -python = ">=3.8.0,<3.12" +python = ">=3.8.0,<3.13" requests = "^2.31.0" numpy = ">= 1.13.0" numpydoc = "*" From ee8f70a323c1528e71b1d2b2f24abb3ca2394695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:01:42 +0100 Subject: [PATCH 09/14] Update release notes --- RELEASE_NOTES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4cafea89..78d250a3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,14 @@ # TerminusDB Python Client Release Notes +## v11.1.0 + +- Add support for python 3.12 +- Aligned with TerminusDB 11.1 overall and general preparation for v11.2.0 + +### Bug fixes + +- Fix schema parameter to database construction + ## v10.2.6 ### Bug fixes From 2347476de780083a17dfe81095d961e859af95b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:20:57 +0100 Subject: [PATCH 10/14] Add release_steps notes --- docs/release_steps.md | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/release_steps.md diff --git a/docs/release_steps.md b/docs/release_steps.md new file mode 100644 index 00000000..9caaac19 --- /dev/null +++ b/docs/release_steps.md @@ -0,0 +1,50 @@ +# Release Steps + +## Quick Release + +1. Update release notes (`RELEASE_NOTES.md`) +2. Bump version (bumpversion [patch|minor|major]) +3. Push new tag (`git push origin main --tags`) + +### Bump version (updates files + creates tag) + +```bash +bumpversion [patch|minor|major] +``` + +### Create and push tag (triggers automated PyPI deployment) + +```bash +git tag -a vX.Y.Z -m "Version X.Y.Z" +git push origin main --tags +``` + +**Monitor**: https://github.com/terminusdb/terminusdb-client-python/actions + +## Details + +### What bumpversion updates +- `terminusdb_client/__version__.py` +- `pyproject.toml` +- `.bumpversion.cfg` + +### Automated deployment +Pushing a tag triggers GitHub Actions to: +- Run tests (Python 3.8-3.12) +- Build with Poetry +- Publish to PyPI + +### Manual deployment (if needed) +```bash +poetry build +poetry publish +``` + +### Troubleshooting + +**Version conflicts:** Never delete published PyPI versions. Create a new patch release instead. + +## Prerequisites + +- Install: `pip install bumpversion` +- PyPI publishing handled via `PYPI_API_TOKEN` secret in GitHub Actions From 8c781b0e1886e0f250378d6bad639877040c89de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:21:27 +0100 Subject: [PATCH 11/14] Update release_steps --- docs/release_steps.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release_steps.md b/docs/release_steps.md index 9caaac19..0a2c0aa5 100644 --- a/docs/release_steps.md +++ b/docs/release_steps.md @@ -8,6 +8,8 @@ ### Bump version (updates files + creates tag) +To not create a tag, use `--no-tag`. + ```bash bumpversion [patch|minor|major] ``` @@ -15,7 +17,6 @@ bumpversion [patch|minor|major] ### Create and push tag (triggers automated PyPI deployment) ```bash -git tag -a vX.Y.Z -m "Version X.Y.Z" git push origin main --tags ``` From 5d36cb7b7230b6c5590a06f22da7b78b7f54fce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:32:55 +0100 Subject: [PATCH 12/14] Update the readme --- README.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c795d408..aa807253 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![downloads](https://img.shields.io/pypi/dm/terminusdb-client.svg?logo=pypi)](https://pypi.python.org/pypi/terminusdb-client/) [![build status](https://img.shields.io/github/workflow/status/terminusdb/terminusdb-client-python/Python%20package?logo=github)](https://github.com/terminusdb/terminusdb-client-python/actions) -[![documentation](https://img.shields.io/github/deployments/terminusdb/terminusdb-client-python/github-pages?label=documentation&logo=github)](https://terminusdb.com/docs/python) +[![documentation](https://img.shields.io/github/deployments/terminusdb/terminusdb-client-python/github-pages?label=documentation&logo=github)](https://terminusdb.org/docs/python) [![code coverage](https://codecov.io/gh/terminusdb/terminusdb-client-python/branch/main/graph/badge.svg?token=BclAUaOPnQ)](https://codecov.io/gh/terminusdb/terminusdb-client-python) [![license](https://img.shields.io/github/license/terminusdb/terminusdb-client-python?color=pink&logo=apache)](https://github.com/terminusdb/terminusdb-client-python/blob/main/LICENSE) @@ -20,24 +20,20 @@ [**TerminusDB**][terminusdb] is an [open-source][terminusdb-repo] graph database and document store. It allows you to link JSON documents in a powerful knowledge -graph all through a simple document API. +graph all through a simple document API, with full git-for-data version control. -[terminusdb]: https://terminusdb.com/ -[terminusdb-docs]: https://terminusdb.com/docs/ +[terminusdb]: https://terminusdb.org/ +[terminusdb-docs]: https://terminusdb.org/docs/ [terminusdb-repo]: https://github.com/terminusdb/terminusdb -[**TerminusCMS**](https://terminusdb.com/terminuscms/) is a hosted headless content management system. It is built upon TerminusDB and is a developer-focused data management platform for complex data and content infrastructure. [Sign up and clone a demo project to see how it works][dashboard]. - -[dashboard]: https://dashboard.terminusdb.com/ - ## Requirements -- [TerminusDB v10.0](https://github.com/terminusdb/terminusdb-server) +- [TerminusDB v11.1](https://github.com/terminusdb/terminusdb-server) - [Python >=3.8](https://www.python.org/downloads) ## Release Notes and Previous Versions -TerminusDB Client v10.0 works with TerminusDB v10.0 and TerminusCMS. Please check the [Release Notes](RELEASE_NOTES.md) to find out what has changed. +TerminusDB Client v11.1 works with TerminusDB v11.1 and the [DFRNT cloud service](https://dfrnt.com). Please check the [Release Notes](RELEASE_NOTES.md) to find out what has changed. ## Installation - TerminusDB Client can be downloaded from PyPI using pip: @@ -76,16 +72,16 @@ client = Client("http://127.0.0.1:6363/") client.connect() ``` -Connect to TerminusCMS +Connect to TerminusDB in the cloud -*check the documentation for TerminusCMS about how to add the [API token](https://terminusdb.com/docs/how-to-connect-terminuscms) to the environment variable* +*check the documentation on the DFRNT support page about how to add your [API token](https://support.dfrnt.com/portal/en/kb/articles/api) to the environment variable* ```Python from terminusdb_client import Client team="MyTeam" -client = Client(f"https://dashboard.terminusdb.com/{team}/") +client = Client(f"https://studio.dfrnt.com/api/hosted/{team}/") client.connect(team="MyTeam", use_token=True) ``` @@ -185,11 +181,11 @@ Do you want to delete 'mydb'? WARNING: This operation is non-reversible. [y/N]: mydb deleted. ``` -### Please check the [full Documentation](https://terminusdb.com/docs/python) for more information. +### Please check the [full Documentation](https://terminusdb.org/docs/python) for more information. ## Guides & Tutorials -Visit our documentation for a range of short how-to guides, [how-to use the Python Client](https://terminusdb.com/docs/use-the-python-client) and [how to use the collaboration features with the Python Client](https://terminusdb.com/docs/collaboration-with-python-client). Alternatively, undertake the [Getting Started with the Python Client Tutorial Series.](https://github.com/terminusdb/terminusdb-tutorials/blob/main/getting_started/python-client/README.md). +Visit our documentation for a range of short how-to guides, [how-to use the Python Client](https://terminusdb.org/docs/use-the-python-client) and [how to use the collaboration features with the Python Client](https://terminusdb.org/docs/collaboration-with-python-client). Alternatively, undertake the [Getting Started with the Python Client Tutorial Series.](https://github.com/terminusdb/terminusdb-tutorials/blob/main/getting_started/python-client/README.md). ## Testing From 8ec60d83d1951cba59724b51d540fdea9152dc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:50:06 +0100 Subject: [PATCH 13/14] Increase client test coverage --- terminusdb_client/tests/test_errors.py | 234 ++++++++++ terminusdb_client/tests/test_query_syntax.py | 80 ++++ .../tests/test_schema_template.py | 159 +++++++ terminusdb_client/tests/test_scripts_main.py | 27 ++ .../tests/test_woql_query_basics.py | 262 +++++++++++ .../tests/test_woql_query_methods.py | 418 ++++++++++++++++++ terminusdb_client/tests/test_woql_utils.py | 290 ++++++++++++ terminusdb_client/tests/test_woqldataframe.py | 174 ++++++++ 8 files changed, 1644 insertions(+) create mode 100644 terminusdb_client/tests/test_errors.py create mode 100644 terminusdb_client/tests/test_query_syntax.py create mode 100644 terminusdb_client/tests/test_schema_template.py create mode 100644 terminusdb_client/tests/test_scripts_main.py create mode 100644 terminusdb_client/tests/test_woql_query_basics.py create mode 100644 terminusdb_client/tests/test_woql_query_methods.py create mode 100644 terminusdb_client/tests/test_woql_utils.py create mode 100644 terminusdb_client/tests/test_woqldataframe.py diff --git a/terminusdb_client/tests/test_errors.py b/terminusdb_client/tests/test_errors.py new file mode 100644 index 00000000..0ca2396a --- /dev/null +++ b/terminusdb_client/tests/test_errors.py @@ -0,0 +1,234 @@ +"""Tests for errors.py module.""" +import json +from unittest.mock import Mock +from terminusdb_client.errors import ( + Error, + InterfaceError, + DatabaseError, + OperationalError, + AccessDeniedError, + APIError, + InvalidURIError +) + + +def test_base_error_class(): + """Test base Error class can be instantiated.""" + error = Error() + assert isinstance(error, Exception) + + +def test_interface_error(): + """Test InterfaceError with message.""" + message = "Database interface error occurred" + error = InterfaceError(message) + + assert isinstance(error, Error) + assert error.message == message + + +def test_interface_error_string_representation(): + """Test InterfaceError has message attribute.""" + message = "Connection failed" + error = InterfaceError(message) + + assert hasattr(error, 'message') + assert error.message == message + + +def test_database_error_with_empty_response(): + """Test DatabaseError with empty response text.""" + mock_response = Mock() + mock_response.text = "" + mock_response.status_code = 500 + + error = DatabaseError(mock_response) + + assert "Unknown Error" in error.message + assert error.status_code == 500 + + +def test_database_error_with_json_api_message(): + """Test DatabaseError with JSON response containing api:message.""" + mock_response = Mock() + mock_response.text = "Error response" + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = { + "api:message": "Database operation failed", + "details": "Additional context" + } + mock_response.status_code = 400 + + error = DatabaseError(mock_response) + + assert "Database operation failed" in error.message + assert error.status_code == 400 + assert error.error_obj == mock_response.json() + + +def test_database_error_with_vio_message(): + """Test DatabaseError with JSON response containing vio:message.""" + mock_response = Mock() + mock_response.text = "Error response" + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = { + "api:error": { + "vio:message": "Validation failed" + } + } + mock_response.status_code = 422 + + error = DatabaseError(mock_response) + + assert "Validation failed" in error.message + assert error.status_code == 422 + + +def test_database_error_with_unknown_json(): + """Test DatabaseError with JSON response without known message fields.""" + mock_response = Mock() + mock_response.text = "Error response" + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = { + "unknown_field": "some value" + } + mock_response.status_code = 500 + + error = DatabaseError(mock_response) + + assert "Unknown Error" in error.message + assert error.status_code == 500 + + +def test_database_error_with_non_json_response(): + """Test DatabaseError with non-JSON response.""" + mock_response = Mock() + mock_response.text = "Plain text error message" + mock_response.headers = {"content-type": "text/plain"} + mock_response.status_code = 503 + + error = DatabaseError(mock_response) + + assert error.message == "Plain text error message" + assert error.error_obj is None + assert error.status_code == 503 + + +def test_database_error_string_representation(): + """Test DatabaseError __str__ method.""" + mock_response = Mock() + mock_response.text = "Error message" + mock_response.headers = {"content-type": "text/plain"} + mock_response.status_code = 500 + + error = DatabaseError(mock_response) + + assert str(error) == error.message + + +def test_operational_error(): + """Test OperationalError inherits from DatabaseError.""" + mock_response = Mock() + mock_response.text = "Operational error" + mock_response.headers = {"content-type": "text/plain"} + mock_response.status_code = 500 + + error = OperationalError(mock_response) + + assert isinstance(error, DatabaseError) + assert isinstance(error, Error) + assert error.message == "Operational error" + + +def test_access_denied_error(): + """Test AccessDeniedError inherits from DatabaseError.""" + mock_response = Mock() + mock_response.text = "Access denied" + mock_response.headers = {"content-type": "text/plain"} + mock_response.status_code = 403 + + error = AccessDeniedError(mock_response) + + assert isinstance(error, DatabaseError) + assert isinstance(error, Error) + assert error.message == "Access denied" + assert error.status_code == 403 + + +def test_api_error_class_exists(): + """Test that APIError class exists and inherits from DatabaseError.""" + # APIError has a design issue where it calls super().__init__() without response + # Just verify the class exists and has correct inheritance + assert issubclass(APIError, DatabaseError) + assert issubclass(APIError, Error) + + +def test_invalid_uri_error(): + """Test InvalidURIError can be instantiated.""" + error = InvalidURIError() + + assert isinstance(error, Error) + assert isinstance(error, Exception) + + +def test_invalid_uri_error_is_simple_error(): + """Test InvalidURIError is a simple pass-through error.""" + # InvalidURIError is a simple pass class, no custom __init__ + error = InvalidURIError() + + assert isinstance(error, Error) + assert isinstance(error, Exception) + + +def test_database_error_json_formatting(): + """Test that DatabaseError includes formatted JSON details.""" + mock_response = Mock() + mock_response.text = "Error" + mock_response.headers = {"content-type": "application/json"} + error_data = { + "api:message": "Error message", + "code": "ERR_001" + } + mock_response.json.return_value = error_data + mock_response.status_code = 400 + + error = DatabaseError(mock_response) + + # Check that formatted JSON is in the message + formatted = json.dumps(error_data, indent=4, sort_keys=True) + assert formatted in error.message + + +def test_all_error_classes_are_exceptions(): + """Test that all error classes inherit from Exception.""" + errors = [ + Error(), + InterfaceError("test"), + InvalidURIError() + ] + + for error in errors: + assert isinstance(error, Exception) + + +def test_error_inheritance_chain(): + """Test proper inheritance chain for database errors.""" + mock_response = Mock() + mock_response.text = "test" + mock_response.headers = {"content-type": "text/plain"} + mock_response.status_code = 500 + + operational = OperationalError(mock_response) + access_denied = AccessDeniedError(mock_response) + + # All should be DatabaseError + assert isinstance(operational, DatabaseError) + assert isinstance(access_denied, DatabaseError) + + # All should be Error + assert isinstance(operational, Error) + assert isinstance(access_denied, Error) + + # All should be Exception + assert isinstance(operational, Exception) + assert isinstance(access_denied, Exception) diff --git a/terminusdb_client/tests/test_query_syntax.py b/terminusdb_client/tests/test_query_syntax.py new file mode 100644 index 00000000..517be136 --- /dev/null +++ b/terminusdb_client/tests/test_query_syntax.py @@ -0,0 +1,80 @@ +"""Tests for query_syntax/query_syntax.py module.""" +from terminusdb_client.query_syntax import query_syntax +from terminusdb_client.woqlquery import WOQLQuery + + +def test_query_syntax_exports_var(): + """Test that Var is exported.""" + assert 'Var' in query_syntax.__all__ + assert hasattr(query_syntax, 'Var') + + +def test_query_syntax_exports_vars(): + """Test that Vars is exported.""" + assert 'Vars' in query_syntax.__all__ + assert hasattr(query_syntax, 'Vars') + + +def test_query_syntax_exports_doc(): + """Test that Doc is exported.""" + assert 'Doc' in query_syntax.__all__ + assert hasattr(query_syntax, 'Doc') + + +def test_barred_items_not_exported(): + """Test that barred items (re, vars) are not exported.""" + assert 're' not in query_syntax.__all__ + assert 'vars' not in query_syntax.__all__ + + +def test_allowed_dunder_methods_exported(): + """Test that allowed dunder methods are exported.""" + # __and__, __or__, __add__ should be exported + assert '__and__' in query_syntax.__all__ + assert '__or__' in query_syntax.__all__ + assert '__add__' in query_syntax.__all__ + + +def test_dynamic_function_creation(): + """Test that functions are dynamically created from WOQLQuery.""" + # Check that some common WOQLQuery methods are available + woql_methods = ['triple', 'select', 'limit'] + + for method in woql_methods: + assert hasattr(query_syntax, method), f"{method} should be available" + assert method in query_syntax.__all__, f"{method} should be in __all__" + + +def test_created_functions_are_callable(): + """Test that dynamically created functions are callable.""" + # Test that we can call a dynamically created function + if hasattr(query_syntax, 'limit'): + func = getattr(query_syntax, 'limit') + assert callable(func) + + +def test_created_function_returns_woqlquery(): + """Test that created functions return WOQLQuery instances.""" + # Test a simple function that exists on WOQLQuery + if hasattr(query_syntax, 'limit'): + func = getattr(query_syntax, 'limit') + result = func(10) + # Should return a WOQLQuery or similar object + assert result is not None + + +def test_private_attributes_not_exported(): + """Test that private attributes (starting with _) are not exported.""" + for attr in query_syntax.__all__: + # All exported attributes should either be in __ALLOWED or not start with _ + if attr not in ['__and__', '__or__', '__add__']: + assert not attr.startswith('_'), f"{attr} should not start with underscore" + + +def test_module_attributes(): + """Test module has expected attributes.""" + # These are defined in the module + assert hasattr(query_syntax, '__BARRED') + assert hasattr(query_syntax, '__ALLOWED') + assert hasattr(query_syntax, '__module') + assert hasattr(query_syntax, '__exported') diff --git a/terminusdb_client/tests/test_schema_template.py b/terminusdb_client/tests/test_schema_template.py new file mode 100644 index 00000000..f2405bd7 --- /dev/null +++ b/terminusdb_client/tests/test_schema_template.py @@ -0,0 +1,159 @@ +"""Tests for scripts/schema_template.py module.""" +from terminusdb_client.scripts import schema_template +from terminusdb_client.woqlschema import DocumentTemplate, EnumTemplate, TaggedUnion + + +def test_schema_template_imports(): + """Test that schema_template module can be imported.""" + assert schema_template is not None + assert schema_template.__doc__ is not None + + +def test_country_class_exists(): + """Test that Country class is defined.""" + assert hasattr(schema_template, 'Country') + assert issubclass(schema_template.Country, DocumentTemplate) + + +def test_country_has_key(): + """Test that Country has HashKey configured.""" + assert hasattr(schema_template.Country, '_key') + + +def test_country_attributes(): + """Test that Country has expected attributes.""" + country = schema_template.Country + # Check type hints exist + assert hasattr(country, '__annotations__') + assert 'name' in country.__annotations__ + assert 'also_know_as' in country.__annotations__ + + +def test_address_class_exists(): + """Test that Address class is defined.""" + assert hasattr(schema_template, 'Address') + assert issubclass(schema_template.Address, DocumentTemplate) + + +def test_address_is_subdocument(): + """Test that Address is configured as subdocument.""" + assert hasattr(schema_template.Address, '_subdocument') + + +def test_address_attributes(): + """Test that Address has expected attributes.""" + address = schema_template.Address + assert 'street' in address.__annotations__ + assert 'postal_code' in address.__annotations__ + assert 'country' in address.__annotations__ + + +def test_person_class_exists(): + """Test that Person class is defined.""" + assert hasattr(schema_template, 'Person') + assert issubclass(schema_template.Person, DocumentTemplate) + + +def test_person_has_docstring(): + """Test that Person has numpydoc formatted docstring.""" + assert schema_template.Person.__doc__ is not None + assert 'Attributes' in schema_template.Person.__doc__ + + +def test_person_attributes(): + """Test that Person has expected attributes.""" + person = schema_template.Person + assert 'name' in person.__annotations__ + assert 'age' in person.__annotations__ + assert 'friend_of' in person.__annotations__ + + +def test_employee_inherits_person(): + """Test that Employee inherits from Person.""" + assert hasattr(schema_template, 'Employee') + assert issubclass(schema_template.Employee, schema_template.Person) + + +def test_employee_attributes(): + """Test that Employee has expected attributes.""" + employee = schema_template.Employee + assert 'address_of' in employee.__annotations__ + assert 'contact_number' in employee.__annotations__ + assert 'managed_by' in employee.__annotations__ + + +def test_coordinate_class_exists(): + """Test that Coordinate class is defined.""" + assert hasattr(schema_template, 'Coordinate') + assert issubclass(schema_template.Coordinate, DocumentTemplate) + + +def test_coordinate_is_abstract(): + """Test that Coordinate is configured as abstract.""" + assert hasattr(schema_template.Coordinate, '_abstract') + + +def test_coordinate_attributes(): + """Test that Coordinate has x and y attributes.""" + coordinate = schema_template.Coordinate + assert 'x' in coordinate.__annotations__ + assert 'y' in coordinate.__annotations__ + + +def test_location_multiple_inheritance(): + """Test that Location inherits from both Address and Coordinate.""" + assert hasattr(schema_template, 'Location') + assert issubclass(schema_template.Location, schema_template.Address) + assert issubclass(schema_template.Location, schema_template.Coordinate) + + +def test_location_attributes(): + """Test that Location has its own attributes.""" + location = schema_template.Location + assert 'name' in location.__annotations__ + + +def test_team_enum_exists(): + """Test that Team enum is defined.""" + assert hasattr(schema_template, 'Team') + assert issubclass(schema_template.Team, EnumTemplate) + + +def test_team_enum_values(): + """Test that Team has expected enum values.""" + team = schema_template.Team + assert hasattr(team, 'IT') + assert hasattr(team, 'Marketing') + assert team.IT.value == "Information Technology" + + +def test_contact_tagged_union_exists(): + """Test that Contact tagged union is defined.""" + assert hasattr(schema_template, 'Contact') + assert issubclass(schema_template.Contact, TaggedUnion) + + +def test_contact_union_attributes(): + """Test that Contact has union type attributes.""" + contact = schema_template.Contact + assert 'local_number' in contact.__annotations__ + assert 'international' in contact.__annotations__ + + +def test_all_classes_importable(): + """Test that all template classes can be imported.""" + classes = [ + 'Country', 'Address', 'Person', 'Employee', + 'Coordinate', 'Location', 'Team', 'Contact' + ] + for cls_name in classes: + assert hasattr(schema_template, cls_name), f"{cls_name} should be importable" + + +def test_module_docstring(): + """Test that module has docstring with metadata.""" + doc = schema_template.__doc__ + assert doc is not None + assert 'Title:' in doc + assert 'Description:' in doc + assert 'Authors:' in doc diff --git a/terminusdb_client/tests/test_scripts_main.py b/terminusdb_client/tests/test_scripts_main.py new file mode 100644 index 00000000..60bf5603 --- /dev/null +++ b/terminusdb_client/tests/test_scripts_main.py @@ -0,0 +1,27 @@ +"""Tests for scripts/__main__.py module.""" +import sys +from unittest.mock import patch, MagicMock + + +def test_main_imports(): + """Test that __main__ can be imported.""" + from terminusdb_client.scripts import __main__ + assert __main__ is not None + + +def test_main_has_tdbpy(): + """Test that __main__ imports tdbpy function.""" + from terminusdb_client.scripts import __main__ + assert hasattr(__main__, 'tdbpy') + + +def test_main_execution(): + """Test that __main__ calls tdbpy when executed.""" + mock_tdbpy = MagicMock() + + with patch('terminusdb_client.scripts.__main__.tdbpy', mock_tdbpy): + # Simulate running as main module + with patch.object(sys, 'argv', ['__main__.py']): + # Import and check the condition would trigger + from terminusdb_client.scripts import __main__ + assert __main__.__name__ == 'terminusdb_client.scripts.__main__' diff --git a/terminusdb_client/tests/test_woql_query_basics.py b/terminusdb_client/tests/test_woql_query_basics.py new file mode 100644 index 00000000..a922e363 --- /dev/null +++ b/terminusdb_client/tests/test_woql_query_basics.py @@ -0,0 +1,262 @@ +"""Tests for basic woql_query.py classes and functions.""" +from terminusdb_client.woqlquery.woql_query import Var, Vars, Doc, SHORT_NAME_MAPPING + + +def test_var_creation(): + """Test Var class instantiation.""" + var = Var("myVar") + + assert var.name == "myVar" + + +def test_var_str(): + """Test Var __str__ method.""" + var = Var("testVar") + + assert str(var) == "testVar" + + +def test_var_to_dict(): + """Test Var to_dict method.""" + var = Var("myVariable") + + result = var.to_dict() + + assert result == {"@type": "Value", "variable": "myVariable"} + + +def test_vars_creation(): + """Test Vars class creates attributes for each arg.""" + vars_obj = Vars("x", "y", "z") + + assert hasattr(vars_obj, "x") + assert hasattr(vars_obj, "y") + assert hasattr(vars_obj, "z") + assert isinstance(vars_obj.x, Var) + assert vars_obj.x.name == "x" + assert vars_obj.y.name == "y" + assert vars_obj.z.name == "z" + + +def test_vars_empty(): + """Test Vars with no arguments.""" + vars_obj = Vars() + + # Should create object successfully even with no args + assert vars_obj is not None + + +def test_doc_with_string(): + """Test Doc class with string value.""" + doc = Doc("test string") + + result = doc.to_dict() + + assert result == { + "@type": "Value", + "data": {"@type": "xsd:string", "@value": "test string"} + } + + +def test_doc_with_bool(): + """Test Doc class with boolean value.""" + doc_true = Doc(True) + doc_false = Doc(False) + + assert doc_true.to_dict() == { + "@type": "Value", + "data": {"@type": "xsd:boolean", "@value": True} + } + assert doc_false.to_dict() == { + "@type": "Value", + "data": {"@type": "xsd:boolean", "@value": False} + } + + +def test_doc_with_int(): + """Test Doc class with integer value.""" + doc = Doc(42) + + result = doc.to_dict() + + assert result == { + "@type": "Value", + "data": {"@type": "xsd:integer", "@value": 42} + } + + +def test_doc_with_float(): + """Test Doc class with float value.""" + doc = Doc(3.14) + + result = doc.to_dict() + + assert result == { + "@type": "Value", + "data": {"@type": "xsd:decimal", "@value": 3.14} + } + + +def test_doc_with_none(): + """Test Doc class with None value.""" + doc = Doc(None) + + result = doc.to_dict() + + assert result is None + + +def test_doc_with_list(): + """Test Doc class with list value.""" + doc = Doc([1, "test", True]) + + result = doc.to_dict() + + assert result["@type"] == "Value" + assert "list" in result + assert len(result["list"]) == 3 + # Check first element + assert result["list"][0] == { + "@type": "Value", + "data": {"@type": "xsd:integer", "@value": 1} + } + + +def test_doc_with_var(): + """Test Doc class with Var value.""" + var = Var("myVar") + doc = Doc(var) + + result = doc.to_dict() + + assert result == { + "@type": "Value", + "variable": "myVar" + } + + +def test_doc_with_dict(): + """Test Doc class with dictionary value.""" + doc = Doc({"name": "Alice", "age": 30}) + + result = doc.to_dict() + + assert result["@type"] == "Value" + assert "dictionary" in result + assert result["dictionary"]["@type"] == "DictionaryTemplate" + assert len(result["dictionary"]["data"]) == 2 + + # Find the name field + name_pair = next(p for p in result["dictionary"]["data"] if p["field"] == "name") + assert name_pair["value"] == { + "@type": "Value", + "data": {"@type": "xsd:string", "@value": "Alice"} + } + + +def test_doc_str(): + """Test Doc __str__ method.""" + doc = Doc({"key": "value"}) + + result = str(doc) + + assert result == "{'key': 'value'}" + + +def test_doc_nested_structures(): + """Test Doc with nested list and dict.""" + doc = Doc({ + "list": [1, 2, 3], + "nested": {"inner": "value"} + }) + + result = doc.to_dict() + + assert result["@type"] == "Value" + assert "dictionary" in result + + +def test_short_name_mapping_exists(): + """Test SHORT_NAME_MAPPING dictionary exists and has expected keys.""" + assert "type" in SHORT_NAME_MAPPING + assert SHORT_NAME_MAPPING["type"] == "rdf:type" + + assert "label" in SHORT_NAME_MAPPING + assert SHORT_NAME_MAPPING["label"] == "rdfs:label" + + assert "string" in SHORT_NAME_MAPPING + assert SHORT_NAME_MAPPING["string"] == "xsd:string" + + assert "integer" in SHORT_NAME_MAPPING + assert SHORT_NAME_MAPPING["integer"] == "xsd:integer" + + +def test_doc_with_empty_list(): + """Test Doc with empty list.""" + doc = Doc([]) + + result = doc.to_dict() + + assert result == {"@type": "Value", "list": []} + + +def test_doc_with_empty_dict(): + """Test Doc with empty dictionary.""" + doc = Doc({}) + + result = doc.to_dict() + + assert result == { + "@type": "Value", + "dictionary": {"@type": "DictionaryTemplate", "data": []} + } + + +def test_doc_with_nested_var(): + """Test Doc with Var inside a list.""" + var = Var("x") + doc = Doc([1, var, "test"]) + + result = doc.to_dict() + + assert result["list"][1] == {"@type": "Value", "variable": "x"} + + +def test_doc_with_mixed_types(): + """Test Doc with various mixed types in dict.""" + var = Var("status") + doc = Doc({ + "id": 1, + "name": "Test", + "active": True, + "score": 95.5, + "status": var, + "tags": ["a", "b"], + "metadata": None + }) + + result = doc.to_dict() + + assert result["@type"] == "Value" + assert "dictionary" in result + assert result["dictionary"]["@type"] == "DictionaryTemplate" + assert len(result["dictionary"]["data"]) == 7 + + +def test_vars_multiple_creation(): + """Test Vars creates multiple Var instances.""" + v = Vars("name", "age", "city") + + # All should be Var instances + assert isinstance(v.name, Var) + assert isinstance(v.age, Var) + assert isinstance(v.city, Var) + + # Each should have correct name + assert v.name.name == "name" + assert v.age.name == "age" + assert v.city.name == "city" + + # They should be different objects + assert v.name is not v.age + assert v.age is not v.city diff --git a/terminusdb_client/tests/test_woql_query_methods.py b/terminusdb_client/tests/test_woql_query_methods.py new file mode 100644 index 00000000..0353d5f2 --- /dev/null +++ b/terminusdb_client/tests/test_woql_query_methods.py @@ -0,0 +1,418 @@ +"""Tests for WOQLQuery class methods in woql_query.py.""" +from terminusdb_client.woqlquery.woql_query import WOQLQuery, Var + + +def test_woqlquery_initialization_empty(): + """Test WOQLQuery initialization with no arguments.""" + query = WOQLQuery() + + assert query._query == {} + assert query._cursor == query._query + assert query._chain_ended is False + assert query._contains_update is False + assert query._graph == "schema" + + +def test_woqlquery_initialization_with_query(): + """Test WOQLQuery initialization with query dict.""" + initial_query = {"@type": "Triple", "subject": "v:X"} + query = WOQLQuery(query=initial_query) + + assert query._query == initial_query + assert query._cursor == query._query + + +def test_woqlquery_initialization_with_graph(): + """Test WOQLQuery initialization with custom graph.""" + query = WOQLQuery(graph="instance") + + assert query._graph == "instance" + + +def test_woqlquery_aliases(): + """Test that WOQLQuery has expected method aliases.""" + query = WOQLQuery() + + # Check aliases exist + assert hasattr(query, 'subsumption') + assert hasattr(query, 'equals') + assert hasattr(query, 'substring') + assert hasattr(query, 'update') + assert hasattr(query, 'delete') + assert hasattr(query, 'read') + assert hasattr(query, 'insert') + assert hasattr(query, 'optional') + assert hasattr(query, 'idgenerator') + assert hasattr(query, 'concatenate') + assert hasattr(query, 'typecast') + + +def test_woqlquery_add_operator(): + """Test WOQLQuery __add__ operator.""" + q1 = WOQLQuery({"@type": "Query1"}) + q2 = WOQLQuery({"@type": "Query2"}) + + result = q1 + q2 + + assert isinstance(result, WOQLQuery) + assert result._query.get("@type") == "And" + + +def test_woqlquery_and_operator(): + """Test WOQLQuery __and__ operator.""" + q1 = WOQLQuery({"@type": "Query1"}) + q2 = WOQLQuery({"@type": "Query2"}) + + result = q1 & q2 + + assert isinstance(result, WOQLQuery) + assert result._query.get("@type") == "And" + + +def test_woqlquery_or_operator(): + """Test WOQLQuery __or__ operator.""" + q1 = WOQLQuery({"@type": "Query1"}) + q2 = WOQLQuery({"@type": "Query2"}) + + result = q1 | q2 + + assert isinstance(result, WOQLQuery) + assert result._query.get("@type") == "Or" + + +def test_woqlquery_invert_operator(): + """Test WOQLQuery __invert__ operator (not).""" + q = WOQLQuery({"@type": "Query"}) + + result = ~q + + assert isinstance(result, WOQLQuery) + assert result._query.get("@type") == "Not" + + +def test_add_sub_query_with_query(): + """Test _add_sub_query with a query object.""" + query = WOQLQuery() + sub_query = {"@type": "Triple"} + + result = query._add_sub_query(sub_query) + + assert result is query # Returns self + assert query._cursor["query"] == sub_query + + +def test_add_sub_query_without_query(): + """Test _add_sub_query creates empty object when no query provided.""" + query = WOQLQuery() + original_cursor = query._cursor + + result = query._add_sub_query() + + assert result is query + assert "query" in original_cursor + # Cursor is moved to the new empty object + assert query._cursor == {} + assert original_cursor["query"] == query._cursor + + +def test_contains_update_check_false(): + """Test _contains_update_check returns False for read query.""" + query = WOQLQuery({"@type": "Triple", "subject": "v:X"}) + + assert query._contains_update_check() is False + + +def test_contains_update_check_true_direct(): + """Test _contains_update_check detects update operators.""" + query = WOQLQuery({"@type": "AddTriple"}) + + assert query._contains_update_check() is True + + +def test_contains_update_check_in_consequent(): + """Test _contains_update_check detects updates in consequent.""" + query = WOQLQuery({ + "@type": "When", + "consequent": {"@type": "DeleteTriple"} + }) + + assert query._contains_update_check() is True + + +def test_contains_update_check_in_nested_query(): + """Test _contains_update_check detects updates in nested query.""" + query = WOQLQuery({ + "@type": "Select", + "query": {"@type": "UpdateObject"} + }) + + assert query._contains_update_check() is True + + +def test_contains_update_check_in_and(): + """Test _contains_update_check detects updates in And clause.""" + query = WOQLQuery({ + "@type": "And", + "and": [ + {"@type": "Triple"}, + {"@type": "AddQuad"} + ] + }) + + assert query._contains_update_check() is True + + +def test_contains_update_check_in_or(): + """Test _contains_update_check detects updates in Or clause.""" + query = WOQLQuery({ + "@type": "Or", + "or": [ + {"@type": "Triple"}, + {"@type": "DeleteObject"} + ] + }) + + assert query._contains_update_check() is True + + +def test_contains_update_check_non_dict(): + """Test _contains_update_check returns False for non-dict.""" + query = WOQLQuery() + + assert query._contains_update_check("not a dict") is False + + +def test_updated_method(): + """Test _updated marks query as containing update.""" + query = WOQLQuery() + + assert query._contains_update is False + result = query._updated() + + assert result is query # Returns self + assert query._contains_update is True + + +def test_jlt_default_type(): + """Test _jlt wraps value with default xsd:string type.""" + query = WOQLQuery() + + result = query._jlt("test value") + + assert result == {"@type": "xsd:string", "@value": "test value"} + + +def test_jlt_custom_type(): + """Test _jlt wraps value with custom type.""" + query = WOQLQuery() + + result = query._jlt(42, "xsd:integer") + + assert result == {"@type": "xsd:integer", "@value": 42} + + +def test_jlt_type_without_prefix(): + """Test _jlt adds xsd: prefix when not present.""" + query = WOQLQuery() + + result = query._jlt(3.14, "decimal") + + assert result == {"@type": "xsd:decimal", "@value": 3.14} + + +def test_varj_with_var_object(): + """Test _varj with Var object.""" + query = WOQLQuery() + var = Var("myVar") + + result = query._varj(var) + + assert result == {"@type": "Value", "variable": "myVar"} + + +def test_varj_with_v_prefix(): + """Test _varj strips v: prefix.""" + query = WOQLQuery() + + result = query._varj("v:myVariable") + + assert result == {"@type": "Value", "variable": "myVariable"} + + +def test_varj_with_plain_string(): + """Test _varj with plain string.""" + query = WOQLQuery() + + result = query._varj("varName") + + assert result == {"@type": "Value", "variable": "varName"} + + +def test_varj_multiple_variations(): + """Test _varj handles various variable formats.""" + query = WOQLQuery() + + # Test multiple cases + assert query._varj(Var("a"))["variable"] == "a" + assert query._varj("v:b")["variable"] == "b" + assert query._varj("c")["variable"] == "c" + + +def test_coerce_to_dict_with_to_dict_method(): + """Test _coerce_to_dict calls to_dict() if available.""" + query = WOQLQuery() + var = Var("x") + + result = query._coerce_to_dict(var) + + assert result == {"@type": "Value", "variable": "x"} + + +def test_coerce_to_dict_with_true(): + """Test _coerce_to_dict handles True specially.""" + query = WOQLQuery() + + result = query._coerce_to_dict(True) + + assert result == {"@type": "True"} + + +def test_coerce_to_dict_with_dict(): + """Test _coerce_to_dict returns dict as-is.""" + query = WOQLQuery() + input_dict = {"@type": "Test"} + + result = query._coerce_to_dict(input_dict) + + assert result == input_dict + + +def test_raw_var_with_var_object(): + """Test _raw_var extracts name from Var object.""" + query = WOQLQuery() + var = Var("myVar") + + result = query._raw_var(var) + + assert result == "myVar" + + +def test_raw_var_with_v_prefix(): + """Test _raw_var strips v: prefix.""" + query = WOQLQuery() + + result = query._raw_var("v:varName") + + assert result == "varName" + + +def test_raw_var_with_plain_string(): + """Test _raw_var returns plain string as-is.""" + query = WOQLQuery() + + result = query._raw_var("plainName") + + assert result == "plainName" + + +def test_raw_var_list(): + """Test _raw_var_list processes list of variables.""" + query = WOQLQuery() + vars_list = [Var("x"), "v:y", "z"] + + result = query._raw_var_list(vars_list) + + assert result == ["x", "y", "z"] + + +def test_asv_with_column_index(): + """Test _asv with integer column index.""" + query = WOQLQuery() + + result = query._asv(0, "varName") + + assert result["@type"] == "Column" + assert result["indicator"]["@type"] == "Indicator" + assert result["indicator"]["index"] == 0 + assert result["variable"] == "varName" + + +def test_asv_with_column_name(): + """Test _asv with string column name.""" + query = WOQLQuery() + + result = query._asv("ColName", "varName") + + assert result["@type"] == "Column" + assert result["indicator"]["@type"] == "Indicator" + assert result["indicator"]["name"] == "ColName" + assert result["variable"] == "varName" + + +def test_asv_with_var_object(): + """Test _asv with Var object as variable name.""" + query = WOQLQuery() + var = Var("myVar") + + result = query._asv("ColName", var) + + assert result["variable"] == "myVar" + + +def test_asv_strips_v_prefix(): + """Test _asv strips v: prefix from variable name.""" + query = WOQLQuery() + + result = query._asv("ColName", "v:varName") + + assert result["variable"] == "varName" + + +def test_asv_with_type(): + """Test _asv includes type when provided.""" + query = WOQLQuery() + + result = query._asv("ColName", "varName", "xsd:string") + + assert result["type"] == "xsd:string" + + +def test_wfrom_with_format(): + """Test _wfrom sets format.""" + query = WOQLQuery() + opts = {"format": "csv"} + + query._wfrom(opts) + + assert query._cursor["format"]["@type"] == "Format" + assert query._cursor["format"]["format_type"]["@value"] == "csv" + + +def test_wfrom_with_format_header(): + """Test _wfrom sets format_header.""" + query = WOQLQuery() + opts = {"format": "csv", "format_header": True} + + query._wfrom(opts) + + assert query._cursor["format"]["format_header"]["@value"] is True + assert query._cursor["format"]["format_header"]["@type"] == "xsd:boolean" + + +def test_wfrom_without_format(): + """Test _wfrom does nothing without format option.""" + query = WOQLQuery() + + query._wfrom(None) + + assert "format" not in query._cursor + + +def test_arop_with_dict(): + """Test _arop returns dict as-is.""" + query = WOQLQuery() + input_dict = {"@type": "Value", "data": 42} + + result = query._arop(input_dict) + + assert result == input_dict diff --git a/terminusdb_client/tests/test_woql_utils.py b/terminusdb_client/tests/test_woql_utils.py new file mode 100644 index 00000000..c9aa77de --- /dev/null +++ b/terminusdb_client/tests/test_woql_utils.py @@ -0,0 +1,290 @@ +"""Tests for woql_utils.py module.""" +import json +from datetime import datetime +from unittest.mock import Mock +import pytest + +from terminusdb_client.woql_utils import ( + _result2stream, + _args_as_payload, + _finish_response, + _clean_list, + _clean_dict, + _dt_list, + _dt_dict +) +from terminusdb_client.errors import DatabaseError + + +def test_result2stream_basic(): + """Test _result2stream with basic JSON objects.""" + result = '{"a": 1}{"b": 2}' + stream = list(_result2stream(result)) + + assert len(stream) == 2 + assert stream[0] == {"a": 1} + assert stream[1] == {"b": 2} + + +def test_result2stream_with_whitespace(): + """Test _result2stream handles whitespace between objects.""" + result = '{"a": 1} \n {"b": 2}' + stream = list(_result2stream(result)) + + assert len(stream) == 2 + assert stream[0] == {"a": 1} + assert stream[1] == {"b": 2} + + +def test_result2stream_empty(): + """Test _result2stream with empty string.""" + result = '' + stream = list(_result2stream(result)) + + assert len(stream) == 0 + + +def test_args_as_payload_filters_none(): + """Test _args_as_payload filters out None values.""" + args = {"a": 1, "b": None, "c": "test"} + result = _args_as_payload(args) + + assert result == {"a": 1, "c": "test"} + assert "b" not in result + + +def test_args_as_payload_filters_falsy(): + """Test _args_as_payload filters out falsy values.""" + args = {"a": 1, "b": 0, "c": "", "d": False, "e": "test"} + result = _args_as_payload(args) + + # Only truthy values remain + assert result == {"a": 1, "e": "test"} + + +def test_finish_response_success(): + """Test _finish_response with 200 status.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "success" + + result = _finish_response(mock_response) + + assert result == "success" + + +def test_finish_response_with_version(): + """Test _finish_response returns version header.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "data" + mock_response.headers = {"Terminusdb-Data-Version": "v1.0"} + + text, version = _finish_response(mock_response, get_version=True) + + assert text == "data" + assert version == "v1.0" + + +def test_finish_response_streaming(): + """Test _finish_response with streaming=True.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.iter_lines.return_value = iter(["line1", "line2"]) + + result = _finish_response(mock_response, streaming=True) + + # Should return iterator + lines = list(result) + assert lines == ["line1", "line2"] + mock_response.iter_lines.assert_called_once() + + +def test_finish_response_error(): + """Test _finish_response raises DatabaseError on error status.""" + mock_response = Mock() + mock_response.status_code = 400 + mock_response.text = "error" + mock_response.headers = {"content-type": "text/plain"} + + with pytest.raises(DatabaseError): + _finish_response(mock_response) + + +def test_clean_list_with_datetime(): + """Test _clean_list converts datetime to isoformat.""" + dt = datetime(2025, 1, 1, 12, 0, 0) + obj = [dt, "string", 123] + + result = _clean_list(obj) + + assert result[0] == dt.isoformat() + assert result[1] == "string" + assert result[2] == 123 + + +def test_clean_list_nested(): + """Test _clean_list with nested structures.""" + dt = datetime(2025, 1, 1) + obj = [ + "string", + {"key": dt}, + [1, 2, dt] + ] + + result = _clean_list(obj) + + assert result[0] == "string" + assert result[1] == {"key": dt.isoformat()} + assert result[2] == [1, 2, dt.isoformat()] + + +def test_clean_dict_with_datetime(): + """Test _clean_dict converts datetime to isoformat.""" + dt = datetime(2025, 1, 1, 12, 0, 0) + obj = { + "date": dt, + "name": "test", + "count": 42 + } + + result = _clean_dict(obj) + + assert result["date"] == dt.isoformat() + assert result["name"] == "test" + assert result["count"] == 42 + + +def test_clean_dict_nested(): + """Test _clean_dict with nested structures.""" + dt = datetime(2025, 1, 1) + obj = { + "name": "test", + "nested": {"date": dt}, + "list": [1, dt, "string"] + } + + result = _clean_dict(obj) + + assert result["name"] == "test" + assert result["nested"] == {"date": dt.isoformat()} + assert result["list"] == [1, dt.isoformat(), "string"] + + +def test_clean_dict_with_iterable(): + """Test _clean_dict handles iterables correctly.""" + obj = { + "tuple": (1, 2, 3), + "list": [4, 5, 6] + } + + result = _clean_dict(obj) + + assert result["tuple"] == [1, 2, 3] + assert result["list"] == [4, 5, 6] + + +def test_dt_list_parses_isoformat(): + """Test _dt_list converts ISO format strings to datetime.""" + obj = ["2025-01-01T12:00:00", "not a date", 123] + + result = _dt_list(obj) + + assert isinstance(result[0], datetime) + assert result[0] == datetime(2025, 1, 1, 12, 0, 0) + assert result[1] == "not a date" + assert result[2] == 123 + + +def test_dt_list_nested(): + """Test _dt_list with nested structures.""" + obj = [ + "2025-01-01", + {"date": "2025-01-01T10:00:00"}, + ["2025-01-01", "text"] + ] + + result = _dt_list(obj) + + assert isinstance(result[0], datetime) + # Note: _dt_list calls _clean_dict on nested dicts, not _dt_dict + # So dates in nested dicts are not parsed + assert result[1] == {"date": "2025-01-01T10:00:00"} + # Note: _dt_list calls _clean_list on nested lists, not _dt_list + # So dates in nested lists are not parsed + assert result[2] == ["2025-01-01", "text"] + + +def test_dt_dict_parses_isoformat(): + """Test _dt_dict converts ISO format strings to datetime.""" + obj = { + "created": "2025-01-01T12:00:00", + "name": "test", + "invalid": "not a date" + } + + result = _dt_dict(obj) + + assert isinstance(result["created"], datetime) + assert result["created"] == datetime(2025, 1, 1, 12, 0, 0) + assert result["name"] == "test" + assert result["invalid"] == "not a date" + + +def test_dt_dict_nested(): + """Test _dt_dict with nested structures.""" + obj = { + "name": "test", + "nested": {"date": "2025-01-01"}, + "list": ["2025-01-01T10:00:00", 123] + } + + result = _dt_dict(obj) + + assert result["name"] == "test" + assert isinstance(result["nested"]["date"], datetime) + assert isinstance(result["list"][0], datetime) + assert result["list"][1] == 123 + + +def test_dt_dict_with_iterable(): + """Test _dt_dict handles iterables with dates.""" + obj = { + "dates": ["2025-01-01", "2025-01-02"], + "mixed": ["2025-01-01", "text", 123] + } + + result = _dt_dict(obj) + + assert isinstance(result["dates"][0], datetime) + assert isinstance(result["dates"][1], datetime) + assert isinstance(result["mixed"][0], datetime) + assert result["mixed"][1] == "text" + + +def test_clean_list_handles_dict_items(): + """Test _clean_list correctly identifies objects with items() method.""" + dt = datetime(2025, 1, 1) + obj = [ + {"key1": "value1"}, + {"key2": dt} + ] + + result = _clean_list(obj) + + assert result[0] == {"key1": "value1"} + assert result[1] == {"key2": dt.isoformat()} + + +def test_dt_list_handles_dict_items(): + """Test _dt_list correctly processes nested dicts.""" + obj = [ + {"date": "2025-01-01"}, + {"name": "test"} + ] + + result = _dt_list(obj) + + # _dt_list calls _clean_dict on dict items + assert result[0] == {"date": "2025-01-01"} + assert result[1] == {"name": "test"} diff --git a/terminusdb_client/tests/test_woqldataframe.py b/terminusdb_client/tests/test_woqldataframe.py new file mode 100644 index 00000000..029b0da3 --- /dev/null +++ b/terminusdb_client/tests/test_woqldataframe.py @@ -0,0 +1,174 @@ +"""Tests for woqldataframe/woqlDataframe.py module.""" +import pytest +from unittest.mock import Mock, MagicMock, patch +from terminusdb_client.woqldataframe.woqlDataframe import result_to_df +from terminusdb_client.errors import InterfaceError + + +def test_result_to_df_requires_pandas(): + """Test that result_to_df raises ImportError when pandas is not available.""" + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module') as mock_import: + mock_import.side_effect = ImportError("No module named 'pandas'") + + with pytest.raises(ImportError) as exc_info: + result_to_df([{"@id": "test", "@type": "Test"}]) + + assert "pandas" in str(exc_info.value).lower() + assert "terminus-client-python[dataframe]" in str(exc_info.value) + + +def test_result_to_df_requires_client_with_max_embed(): + """Test that result_to_df raises ValueError when max_embed_dep > 0 without client.""" + mock_pd = MagicMock() + mock_pd.DataFrame.return_value.from_records.return_value = mock_pd.DataFrame.return_value + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with pytest.raises(ValueError) as exc_info: + result_to_df([{"@id": "test", "@type": "Test"}], max_embed_dep=1) + + assert "client need to be provide" in str(exc_info.value) + + +def test_result_to_df_multiple_types_error(): + """Test that result_to_df raises ValueError for multiple document types.""" + mock_pd = MagicMock() + + # Create mock DataFrame with multiple types + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["Type1", "Type2"] + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with pytest.raises(ValueError) as exc_info: + result_to_df([ + {"@id": "test1", "@type": "Type1"}, + {"@id": "test2", "@type": "Type2"} + ]) + + assert "multiple type" in str(exc_info.value).lower() + + +def test_result_to_df_class_not_in_schema(): + """Test that result_to_df raises InterfaceError when class not found in schema.""" + mock_pd = MagicMock() + mock_client = MagicMock() + + # Setup mock DataFrame + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["UnknownClass"] + mock_df.columns = ["@id", "@type"] + mock_df.rename.return_value = mock_df + mock_df.drop.return_value = mock_df + + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + mock_client.get_existing_classes.return_value = {"KnownClass": {}} + mock_client.db = "testdb" + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with pytest.raises(InterfaceError) as exc_info: + result_to_df( + [{"@id": "test1", "@type": "UnknownClass"}], + max_embed_dep=1, + client=mock_client + ) + + assert "UnknownClass" in str(exc_info.value) + assert "not found" in str(exc_info.value) + + +def test_result_to_df_basic_conversion(): + """Test basic result_to_df conversion without embedding.""" + mock_pd = MagicMock() + + # Setup mock DataFrame + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["Person"] + mock_df.columns = ["@id", "@type", "name"] + mock_df.rename.return_value = mock_df + mock_df.drop.return_value = mock_df + + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + result = result_to_df([ + {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} + ]) + + # Should return the DataFrame + assert result is not None + + +def test_result_to_df_with_keepid(): + """Test result_to_df with keepid=True.""" + mock_pd = MagicMock() + + # Setup mock DataFrame + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["Person"] + mock_df.columns = ["@id", "@type", "name"] + + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + result = result_to_df([ + {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} + ], keepid=True) + + # Should return the DataFrame + assert result is not None + # Should not call rename when keepid=True + mock_df.rename.assert_not_called() + + +def test_result_to_df_requires_client_for_embedding(): + """Test that result_to_df requires client when max_embed_dep > 0.""" + mock_pd = MagicMock() + + # Setup basic mock + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["Person"] + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + # This tests the ValueError raised at line 18-21 + with pytest.raises(ValueError) as exc_info: + result_to_df( + [{"@id": "Person/Jane", "@type": "Person"}], + max_embed_dep=2, # Requires client + client=None # But no client provided + ) + + assert "client need to be provide" in str(exc_info.value) + + +def test_result_to_df_expand_nested_json(): + """Test that result_to_df expands nested JSON structures.""" + mock_pd = MagicMock() + + # Setup mock DataFrame with nested structure + mock_df = MagicMock() + mock_df.__getitem__.return_value.unique.return_value = ["Person"] + mock_df.columns = ["Document id", "address"] + mock_df.rename.return_value = mock_df + mock_df.drop.return_value = mock_df + mock_df.join.return_value = mock_df + + # Mock json_normalize to simulate expansion + mock_expanded = MagicMock() + mock_expanded.columns = ["@id", "street"] + mock_pd.json_normalize.return_value = mock_expanded + + mock_pd.DataFrame.return_value.from_records.return_value = mock_df + + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + result = result_to_df([ + { + "@id": "Person/Jane", + "@type": "Person", + "address": {"@id": "Address/1", "street": "Main St"} + } + ]) + + # json_normalize should be called for expansion + assert mock_pd.json_normalize.called + assert result is not None From 19b9b56c506ad72de8f35ba510cd4cbab145d827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 15 Nov 2025 07:54:37 +0100 Subject: [PATCH 14/14] Fix linting --- terminusdb_client/tests/test_errors.py | 54 +++---- terminusdb_client/tests/test_query_syntax.py | 3 +- terminusdb_client/tests/test_scripts_main.py | 2 +- .../tests/test_woql_query_basics.py | 80 +++++----- .../tests/test_woql_query_methods.py | 140 +++++++++--------- terminusdb_client/tests/test_woql_utils.py | 73 +++++---- terminusdb_client/tests/test_woqldataframe.py | 56 +++---- 7 files changed, 203 insertions(+), 205 deletions(-) diff --git a/terminusdb_client/tests/test_errors.py b/terminusdb_client/tests/test_errors.py index 0ca2396a..69a3b716 100644 --- a/terminusdb_client/tests/test_errors.py +++ b/terminusdb_client/tests/test_errors.py @@ -22,7 +22,7 @@ def test_interface_error(): """Test InterfaceError with message.""" message = "Database interface error occurred" error = InterfaceError(message) - + assert isinstance(error, Error) assert error.message == message @@ -31,7 +31,7 @@ def test_interface_error_string_representation(): """Test InterfaceError has message attribute.""" message = "Connection failed" error = InterfaceError(message) - + assert hasattr(error, 'message') assert error.message == message @@ -41,9 +41,9 @@ def test_database_error_with_empty_response(): mock_response = Mock() mock_response.text = "" mock_response.status_code = 500 - + error = DatabaseError(mock_response) - + assert "Unknown Error" in error.message assert error.status_code == 500 @@ -58,9 +58,9 @@ def test_database_error_with_json_api_message(): "details": "Additional context" } mock_response.status_code = 400 - + error = DatabaseError(mock_response) - + assert "Database operation failed" in error.message assert error.status_code == 400 assert error.error_obj == mock_response.json() @@ -77,9 +77,9 @@ def test_database_error_with_vio_message(): } } mock_response.status_code = 422 - + error = DatabaseError(mock_response) - + assert "Validation failed" in error.message assert error.status_code == 422 @@ -93,9 +93,9 @@ def test_database_error_with_unknown_json(): "unknown_field": "some value" } mock_response.status_code = 500 - + error = DatabaseError(mock_response) - + assert "Unknown Error" in error.message assert error.status_code == 500 @@ -106,9 +106,9 @@ def test_database_error_with_non_json_response(): mock_response.text = "Plain text error message" mock_response.headers = {"content-type": "text/plain"} mock_response.status_code = 503 - + error = DatabaseError(mock_response) - + assert error.message == "Plain text error message" assert error.error_obj is None assert error.status_code == 503 @@ -120,9 +120,9 @@ def test_database_error_string_representation(): mock_response.text = "Error message" mock_response.headers = {"content-type": "text/plain"} mock_response.status_code = 500 - + error = DatabaseError(mock_response) - + assert str(error) == error.message @@ -132,9 +132,9 @@ def test_operational_error(): mock_response.text = "Operational error" mock_response.headers = {"content-type": "text/plain"} mock_response.status_code = 500 - + error = OperationalError(mock_response) - + assert isinstance(error, DatabaseError) assert isinstance(error, Error) assert error.message == "Operational error" @@ -146,9 +146,9 @@ def test_access_denied_error(): mock_response.text = "Access denied" mock_response.headers = {"content-type": "text/plain"} mock_response.status_code = 403 - + error = AccessDeniedError(mock_response) - + assert isinstance(error, DatabaseError) assert isinstance(error, Error) assert error.message == "Access denied" @@ -166,7 +166,7 @@ def test_api_error_class_exists(): def test_invalid_uri_error(): """Test InvalidURIError can be instantiated.""" error = InvalidURIError() - + assert isinstance(error, Error) assert isinstance(error, Exception) @@ -175,7 +175,7 @@ def test_invalid_uri_error_is_simple_error(): """Test InvalidURIError is a simple pass-through error.""" # InvalidURIError is a simple pass class, no custom __init__ error = InvalidURIError() - + assert isinstance(error, Error) assert isinstance(error, Exception) @@ -191,9 +191,9 @@ def test_database_error_json_formatting(): } mock_response.json.return_value = error_data mock_response.status_code = 400 - + error = DatabaseError(mock_response) - + # Check that formatted JSON is in the message formatted = json.dumps(error_data, indent=4, sort_keys=True) assert formatted in error.message @@ -206,7 +206,7 @@ def test_all_error_classes_are_exceptions(): InterfaceError("test"), InvalidURIError() ] - + for error in errors: assert isinstance(error, Exception) @@ -217,18 +217,18 @@ def test_error_inheritance_chain(): mock_response.text = "test" mock_response.headers = {"content-type": "text/plain"} mock_response.status_code = 500 - + operational = OperationalError(mock_response) access_denied = AccessDeniedError(mock_response) - + # All should be DatabaseError assert isinstance(operational, DatabaseError) assert isinstance(access_denied, DatabaseError) - + # All should be Error assert isinstance(operational, Error) assert isinstance(access_denied, Error) - + # All should be Exception assert isinstance(operational, Exception) assert isinstance(access_denied, Exception) diff --git a/terminusdb_client/tests/test_query_syntax.py b/terminusdb_client/tests/test_query_syntax.py index 517be136..e94a8b93 100644 --- a/terminusdb_client/tests/test_query_syntax.py +++ b/terminusdb_client/tests/test_query_syntax.py @@ -1,6 +1,5 @@ """Tests for query_syntax/query_syntax.py module.""" from terminusdb_client.query_syntax import query_syntax -from terminusdb_client.woqlquery import WOQLQuery def test_query_syntax_exports_var(): @@ -39,7 +38,7 @@ def test_dynamic_function_creation(): """Test that functions are dynamically created from WOQLQuery.""" # Check that some common WOQLQuery methods are available woql_methods = ['triple', 'select', 'limit'] - + for method in woql_methods: assert hasattr(query_syntax, method), f"{method} should be available" assert method in query_syntax.__all__, f"{method} should be in __all__" diff --git a/terminusdb_client/tests/test_scripts_main.py b/terminusdb_client/tests/test_scripts_main.py index 60bf5603..b5e18ca6 100644 --- a/terminusdb_client/tests/test_scripts_main.py +++ b/terminusdb_client/tests/test_scripts_main.py @@ -18,7 +18,7 @@ def test_main_has_tdbpy(): def test_main_execution(): """Test that __main__ calls tdbpy when executed.""" mock_tdbpy = MagicMock() - + with patch('terminusdb_client.scripts.__main__.tdbpy', mock_tdbpy): # Simulate running as main module with patch.object(sys, 'argv', ['__main__.py']): diff --git a/terminusdb_client/tests/test_woql_query_basics.py b/terminusdb_client/tests/test_woql_query_basics.py index a922e363..da4e9e5b 100644 --- a/terminusdb_client/tests/test_woql_query_basics.py +++ b/terminusdb_client/tests/test_woql_query_basics.py @@ -5,30 +5,30 @@ def test_var_creation(): """Test Var class instantiation.""" var = Var("myVar") - + assert var.name == "myVar" def test_var_str(): """Test Var __str__ method.""" var = Var("testVar") - + assert str(var) == "testVar" def test_var_to_dict(): """Test Var to_dict method.""" var = Var("myVariable") - + result = var.to_dict() - + assert result == {"@type": "Value", "variable": "myVariable"} def test_vars_creation(): """Test Vars class creates attributes for each arg.""" vars_obj = Vars("x", "y", "z") - + assert hasattr(vars_obj, "x") assert hasattr(vars_obj, "y") assert hasattr(vars_obj, "z") @@ -41,7 +41,7 @@ def test_vars_creation(): def test_vars_empty(): """Test Vars with no arguments.""" vars_obj = Vars() - + # Should create object successfully even with no args assert vars_obj is not None @@ -49,9 +49,9 @@ def test_vars_empty(): def test_doc_with_string(): """Test Doc class with string value.""" doc = Doc("test string") - + result = doc.to_dict() - + assert result == { "@type": "Value", "data": {"@type": "xsd:string", "@value": "test string"} @@ -62,7 +62,7 @@ def test_doc_with_bool(): """Test Doc class with boolean value.""" doc_true = Doc(True) doc_false = Doc(False) - + assert doc_true.to_dict() == { "@type": "Value", "data": {"@type": "xsd:boolean", "@value": True} @@ -76,9 +76,9 @@ def test_doc_with_bool(): def test_doc_with_int(): """Test Doc class with integer value.""" doc = Doc(42) - + result = doc.to_dict() - + assert result == { "@type": "Value", "data": {"@type": "xsd:integer", "@value": 42} @@ -88,9 +88,9 @@ def test_doc_with_int(): def test_doc_with_float(): """Test Doc class with float value.""" doc = Doc(3.14) - + result = doc.to_dict() - + assert result == { "@type": "Value", "data": {"@type": "xsd:decimal", "@value": 3.14} @@ -100,18 +100,18 @@ def test_doc_with_float(): def test_doc_with_none(): """Test Doc class with None value.""" doc = Doc(None) - + result = doc.to_dict() - + assert result is None def test_doc_with_list(): """Test Doc class with list value.""" doc = Doc([1, "test", True]) - + result = doc.to_dict() - + assert result["@type"] == "Value" assert "list" in result assert len(result["list"]) == 3 @@ -126,9 +126,9 @@ def test_doc_with_var(): """Test Doc class with Var value.""" var = Var("myVar") doc = Doc(var) - + result = doc.to_dict() - + assert result == { "@type": "Value", "variable": "myVar" @@ -138,14 +138,14 @@ def test_doc_with_var(): def test_doc_with_dict(): """Test Doc class with dictionary value.""" doc = Doc({"name": "Alice", "age": 30}) - + result = doc.to_dict() - + assert result["@type"] == "Value" assert "dictionary" in result assert result["dictionary"]["@type"] == "DictionaryTemplate" assert len(result["dictionary"]["data"]) == 2 - + # Find the name field name_pair = next(p for p in result["dictionary"]["data"] if p["field"] == "name") assert name_pair["value"] == { @@ -157,9 +157,9 @@ def test_doc_with_dict(): def test_doc_str(): """Test Doc __str__ method.""" doc = Doc({"key": "value"}) - + result = str(doc) - + assert result == "{'key': 'value'}" @@ -169,9 +169,9 @@ def test_doc_nested_structures(): "list": [1, 2, 3], "nested": {"inner": "value"} }) - + result = doc.to_dict() - + assert result["@type"] == "Value" assert "dictionary" in result @@ -180,13 +180,13 @@ def test_short_name_mapping_exists(): """Test SHORT_NAME_MAPPING dictionary exists and has expected keys.""" assert "type" in SHORT_NAME_MAPPING assert SHORT_NAME_MAPPING["type"] == "rdf:type" - + assert "label" in SHORT_NAME_MAPPING assert SHORT_NAME_MAPPING["label"] == "rdfs:label" - + assert "string" in SHORT_NAME_MAPPING assert SHORT_NAME_MAPPING["string"] == "xsd:string" - + assert "integer" in SHORT_NAME_MAPPING assert SHORT_NAME_MAPPING["integer"] == "xsd:integer" @@ -194,18 +194,18 @@ def test_short_name_mapping_exists(): def test_doc_with_empty_list(): """Test Doc with empty list.""" doc = Doc([]) - + result = doc.to_dict() - + assert result == {"@type": "Value", "list": []} def test_doc_with_empty_dict(): """Test Doc with empty dictionary.""" doc = Doc({}) - + result = doc.to_dict() - + assert result == { "@type": "Value", "dictionary": {"@type": "DictionaryTemplate", "data": []} @@ -216,9 +216,9 @@ def test_doc_with_nested_var(): """Test Doc with Var inside a list.""" var = Var("x") doc = Doc([1, var, "test"]) - + result = doc.to_dict() - + assert result["list"][1] == {"@type": "Value", "variable": "x"} @@ -234,9 +234,9 @@ def test_doc_with_mixed_types(): "tags": ["a", "b"], "metadata": None }) - + result = doc.to_dict() - + assert result["@type"] == "Value" assert "dictionary" in result assert result["dictionary"]["@type"] == "DictionaryTemplate" @@ -246,17 +246,17 @@ def test_doc_with_mixed_types(): def test_vars_multiple_creation(): """Test Vars creates multiple Var instances.""" v = Vars("name", "age", "city") - + # All should be Var instances assert isinstance(v.name, Var) assert isinstance(v.age, Var) assert isinstance(v.city, Var) - + # Each should have correct name assert v.name.name == "name" assert v.age.name == "age" assert v.city.name == "city" - + # They should be different objects assert v.name is not v.age assert v.age is not v.city diff --git a/terminusdb_client/tests/test_woql_query_methods.py b/terminusdb_client/tests/test_woql_query_methods.py index 0353d5f2..26f6c3fd 100644 --- a/terminusdb_client/tests/test_woql_query_methods.py +++ b/terminusdb_client/tests/test_woql_query_methods.py @@ -5,7 +5,7 @@ def test_woqlquery_initialization_empty(): """Test WOQLQuery initialization with no arguments.""" query = WOQLQuery() - + assert query._query == {} assert query._cursor == query._query assert query._chain_ended is False @@ -17,7 +17,7 @@ def test_woqlquery_initialization_with_query(): """Test WOQLQuery initialization with query dict.""" initial_query = {"@type": "Triple", "subject": "v:X"} query = WOQLQuery(query=initial_query) - + assert query._query == initial_query assert query._cursor == query._query @@ -25,14 +25,14 @@ def test_woqlquery_initialization_with_query(): def test_woqlquery_initialization_with_graph(): """Test WOQLQuery initialization with custom graph.""" query = WOQLQuery(graph="instance") - + assert query._graph == "instance" def test_woqlquery_aliases(): """Test that WOQLQuery has expected method aliases.""" query = WOQLQuery() - + # Check aliases exist assert hasattr(query, 'subsumption') assert hasattr(query, 'equals') @@ -51,9 +51,9 @@ def test_woqlquery_add_operator(): """Test WOQLQuery __add__ operator.""" q1 = WOQLQuery({"@type": "Query1"}) q2 = WOQLQuery({"@type": "Query2"}) - + result = q1 + q2 - + assert isinstance(result, WOQLQuery) assert result._query.get("@type") == "And" @@ -62,9 +62,9 @@ def test_woqlquery_and_operator(): """Test WOQLQuery __and__ operator.""" q1 = WOQLQuery({"@type": "Query1"}) q2 = WOQLQuery({"@type": "Query2"}) - + result = q1 & q2 - + assert isinstance(result, WOQLQuery) assert result._query.get("@type") == "And" @@ -73,9 +73,9 @@ def test_woqlquery_or_operator(): """Test WOQLQuery __or__ operator.""" q1 = WOQLQuery({"@type": "Query1"}) q2 = WOQLQuery({"@type": "Query2"}) - + result = q1 | q2 - + assert isinstance(result, WOQLQuery) assert result._query.get("@type") == "Or" @@ -83,9 +83,9 @@ def test_woqlquery_or_operator(): def test_woqlquery_invert_operator(): """Test WOQLQuery __invert__ operator (not).""" q = WOQLQuery({"@type": "Query"}) - + result = ~q - + assert isinstance(result, WOQLQuery) assert result._query.get("@type") == "Not" @@ -94,9 +94,9 @@ def test_add_sub_query_with_query(): """Test _add_sub_query with a query object.""" query = WOQLQuery() sub_query = {"@type": "Triple"} - + result = query._add_sub_query(sub_query) - + assert result is query # Returns self assert query._cursor["query"] == sub_query @@ -105,9 +105,9 @@ def test_add_sub_query_without_query(): """Test _add_sub_query creates empty object when no query provided.""" query = WOQLQuery() original_cursor = query._cursor - + result = query._add_sub_query() - + assert result is query assert "query" in original_cursor # Cursor is moved to the new empty object @@ -118,14 +118,14 @@ def test_add_sub_query_without_query(): def test_contains_update_check_false(): """Test _contains_update_check returns False for read query.""" query = WOQLQuery({"@type": "Triple", "subject": "v:X"}) - + assert query._contains_update_check() is False def test_contains_update_check_true_direct(): """Test _contains_update_check detects update operators.""" query = WOQLQuery({"@type": "AddTriple"}) - + assert query._contains_update_check() is True @@ -135,7 +135,7 @@ def test_contains_update_check_in_consequent(): "@type": "When", "consequent": {"@type": "DeleteTriple"} }) - + assert query._contains_update_check() is True @@ -145,7 +145,7 @@ def test_contains_update_check_in_nested_query(): "@type": "Select", "query": {"@type": "UpdateObject"} }) - + assert query._contains_update_check() is True @@ -158,7 +158,7 @@ def test_contains_update_check_in_and(): {"@type": "AddQuad"} ] }) - + assert query._contains_update_check() is True @@ -171,24 +171,24 @@ def test_contains_update_check_in_or(): {"@type": "DeleteObject"} ] }) - + assert query._contains_update_check() is True def test_contains_update_check_non_dict(): """Test _contains_update_check returns False for non-dict.""" query = WOQLQuery() - + assert query._contains_update_check("not a dict") is False def test_updated_method(): """Test _updated marks query as containing update.""" query = WOQLQuery() - + assert query._contains_update is False result = query._updated() - + assert result is query # Returns self assert query._contains_update is True @@ -196,27 +196,27 @@ def test_updated_method(): def test_jlt_default_type(): """Test _jlt wraps value with default xsd:string type.""" query = WOQLQuery() - + result = query._jlt("test value") - + assert result == {"@type": "xsd:string", "@value": "test value"} def test_jlt_custom_type(): """Test _jlt wraps value with custom type.""" query = WOQLQuery() - + result = query._jlt(42, "xsd:integer") - + assert result == {"@type": "xsd:integer", "@value": 42} def test_jlt_type_without_prefix(): """Test _jlt adds xsd: prefix when not present.""" query = WOQLQuery() - + result = query._jlt(3.14, "decimal") - + assert result == {"@type": "xsd:decimal", "@value": 3.14} @@ -224,34 +224,34 @@ def test_varj_with_var_object(): """Test _varj with Var object.""" query = WOQLQuery() var = Var("myVar") - + result = query._varj(var) - + assert result == {"@type": "Value", "variable": "myVar"} def test_varj_with_v_prefix(): """Test _varj strips v: prefix.""" query = WOQLQuery() - + result = query._varj("v:myVariable") - + assert result == {"@type": "Value", "variable": "myVariable"} def test_varj_with_plain_string(): """Test _varj with plain string.""" query = WOQLQuery() - + result = query._varj("varName") - + assert result == {"@type": "Value", "variable": "varName"} def test_varj_multiple_variations(): """Test _varj handles various variable formats.""" query = WOQLQuery() - + # Test multiple cases assert query._varj(Var("a"))["variable"] == "a" assert query._varj("v:b")["variable"] == "b" @@ -262,18 +262,18 @@ def test_coerce_to_dict_with_to_dict_method(): """Test _coerce_to_dict calls to_dict() if available.""" query = WOQLQuery() var = Var("x") - + result = query._coerce_to_dict(var) - + assert result == {"@type": "Value", "variable": "x"} def test_coerce_to_dict_with_true(): """Test _coerce_to_dict handles True specially.""" query = WOQLQuery() - + result = query._coerce_to_dict(True) - + assert result == {"@type": "True"} @@ -281,9 +281,9 @@ def test_coerce_to_dict_with_dict(): """Test _coerce_to_dict returns dict as-is.""" query = WOQLQuery() input_dict = {"@type": "Test"} - + result = query._coerce_to_dict(input_dict) - + assert result == input_dict @@ -291,27 +291,27 @@ def test_raw_var_with_var_object(): """Test _raw_var extracts name from Var object.""" query = WOQLQuery() var = Var("myVar") - + result = query._raw_var(var) - + assert result == "myVar" def test_raw_var_with_v_prefix(): """Test _raw_var strips v: prefix.""" query = WOQLQuery() - + result = query._raw_var("v:varName") - + assert result == "varName" def test_raw_var_with_plain_string(): """Test _raw_var returns plain string as-is.""" query = WOQLQuery() - + result = query._raw_var("plainName") - + assert result == "plainName" @@ -319,18 +319,18 @@ def test_raw_var_list(): """Test _raw_var_list processes list of variables.""" query = WOQLQuery() vars_list = [Var("x"), "v:y", "z"] - + result = query._raw_var_list(vars_list) - + assert result == ["x", "y", "z"] def test_asv_with_column_index(): """Test _asv with integer column index.""" query = WOQLQuery() - + result = query._asv(0, "varName") - + assert result["@type"] == "Column" assert result["indicator"]["@type"] == "Indicator" assert result["indicator"]["index"] == 0 @@ -340,9 +340,9 @@ def test_asv_with_column_index(): def test_asv_with_column_name(): """Test _asv with string column name.""" query = WOQLQuery() - + result = query._asv("ColName", "varName") - + assert result["@type"] == "Column" assert result["indicator"]["@type"] == "Indicator" assert result["indicator"]["name"] == "ColName" @@ -353,27 +353,27 @@ def test_asv_with_var_object(): """Test _asv with Var object as variable name.""" query = WOQLQuery() var = Var("myVar") - + result = query._asv("ColName", var) - + assert result["variable"] == "myVar" def test_asv_strips_v_prefix(): """Test _asv strips v: prefix from variable name.""" query = WOQLQuery() - + result = query._asv("ColName", "v:varName") - + assert result["variable"] == "varName" def test_asv_with_type(): """Test _asv includes type when provided.""" query = WOQLQuery() - + result = query._asv("ColName", "varName", "xsd:string") - + assert result["type"] == "xsd:string" @@ -381,9 +381,9 @@ def test_wfrom_with_format(): """Test _wfrom sets format.""" query = WOQLQuery() opts = {"format": "csv"} - + query._wfrom(opts) - + assert query._cursor["format"]["@type"] == "Format" assert query._cursor["format"]["format_type"]["@value"] == "csv" @@ -392,9 +392,9 @@ def test_wfrom_with_format_header(): """Test _wfrom sets format_header.""" query = WOQLQuery() opts = {"format": "csv", "format_header": True} - + query._wfrom(opts) - + assert query._cursor["format"]["format_header"]["@value"] is True assert query._cursor["format"]["format_header"]["@type"] == "xsd:boolean" @@ -402,9 +402,9 @@ def test_wfrom_with_format_header(): def test_wfrom_without_format(): """Test _wfrom does nothing without format option.""" query = WOQLQuery() - + query._wfrom(None) - + assert "format" not in query._cursor @@ -412,7 +412,7 @@ def test_arop_with_dict(): """Test _arop returns dict as-is.""" query = WOQLQuery() input_dict = {"@type": "Value", "data": 42} - + result = query._arop(input_dict) - + assert result == input_dict diff --git a/terminusdb_client/tests/test_woql_utils.py b/terminusdb_client/tests/test_woql_utils.py index c9aa77de..81ebfdc6 100644 --- a/terminusdb_client/tests/test_woql_utils.py +++ b/terminusdb_client/tests/test_woql_utils.py @@ -1,5 +1,4 @@ """Tests for woql_utils.py module.""" -import json from datetime import datetime from unittest.mock import Mock import pytest @@ -20,7 +19,7 @@ def test_result2stream_basic(): """Test _result2stream with basic JSON objects.""" result = '{"a": 1}{"b": 2}' stream = list(_result2stream(result)) - + assert len(stream) == 2 assert stream[0] == {"a": 1} assert stream[1] == {"b": 2} @@ -30,7 +29,7 @@ def test_result2stream_with_whitespace(): """Test _result2stream handles whitespace between objects.""" result = '{"a": 1} \n {"b": 2}' stream = list(_result2stream(result)) - + assert len(stream) == 2 assert stream[0] == {"a": 1} assert stream[1] == {"b": 2} @@ -40,7 +39,7 @@ def test_result2stream_empty(): """Test _result2stream with empty string.""" result = '' stream = list(_result2stream(result)) - + assert len(stream) == 0 @@ -48,7 +47,7 @@ def test_args_as_payload_filters_none(): """Test _args_as_payload filters out None values.""" args = {"a": 1, "b": None, "c": "test"} result = _args_as_payload(args) - + assert result == {"a": 1, "c": "test"} assert "b" not in result @@ -57,7 +56,7 @@ def test_args_as_payload_filters_falsy(): """Test _args_as_payload filters out falsy values.""" args = {"a": 1, "b": 0, "c": "", "d": False, "e": "test"} result = _args_as_payload(args) - + # Only truthy values remain assert result == {"a": 1, "e": "test"} @@ -67,9 +66,9 @@ def test_finish_response_success(): mock_response = Mock() mock_response.status_code = 200 mock_response.text = "success" - + result = _finish_response(mock_response) - + assert result == "success" @@ -79,9 +78,9 @@ def test_finish_response_with_version(): mock_response.status_code = 200 mock_response.text = "data" mock_response.headers = {"Terminusdb-Data-Version": "v1.0"} - + text, version = _finish_response(mock_response, get_version=True) - + assert text == "data" assert version == "v1.0" @@ -91,9 +90,9 @@ def test_finish_response_streaming(): mock_response = Mock() mock_response.status_code = 200 mock_response.iter_lines.return_value = iter(["line1", "line2"]) - + result = _finish_response(mock_response, streaming=True) - + # Should return iterator lines = list(result) assert lines == ["line1", "line2"] @@ -106,7 +105,7 @@ def test_finish_response_error(): mock_response.status_code = 400 mock_response.text = "error" mock_response.headers = {"content-type": "text/plain"} - + with pytest.raises(DatabaseError): _finish_response(mock_response) @@ -115,9 +114,9 @@ def test_clean_list_with_datetime(): """Test _clean_list converts datetime to isoformat.""" dt = datetime(2025, 1, 1, 12, 0, 0) obj = [dt, "string", 123] - + result = _clean_list(obj) - + assert result[0] == dt.isoformat() assert result[1] == "string" assert result[2] == 123 @@ -131,9 +130,9 @@ def test_clean_list_nested(): {"key": dt}, [1, 2, dt] ] - + result = _clean_list(obj) - + assert result[0] == "string" assert result[1] == {"key": dt.isoformat()} assert result[2] == [1, 2, dt.isoformat()] @@ -147,9 +146,9 @@ def test_clean_dict_with_datetime(): "name": "test", "count": 42 } - + result = _clean_dict(obj) - + assert result["date"] == dt.isoformat() assert result["name"] == "test" assert result["count"] == 42 @@ -163,9 +162,9 @@ def test_clean_dict_nested(): "nested": {"date": dt}, "list": [1, dt, "string"] } - + result = _clean_dict(obj) - + assert result["name"] == "test" assert result["nested"] == {"date": dt.isoformat()} assert result["list"] == [1, dt.isoformat(), "string"] @@ -177,9 +176,9 @@ def test_clean_dict_with_iterable(): "tuple": (1, 2, 3), "list": [4, 5, 6] } - + result = _clean_dict(obj) - + assert result["tuple"] == [1, 2, 3] assert result["list"] == [4, 5, 6] @@ -187,9 +186,9 @@ def test_clean_dict_with_iterable(): def test_dt_list_parses_isoformat(): """Test _dt_list converts ISO format strings to datetime.""" obj = ["2025-01-01T12:00:00", "not a date", 123] - + result = _dt_list(obj) - + assert isinstance(result[0], datetime) assert result[0] == datetime(2025, 1, 1, 12, 0, 0) assert result[1] == "not a date" @@ -203,9 +202,9 @@ def test_dt_list_nested(): {"date": "2025-01-01T10:00:00"}, ["2025-01-01", "text"] ] - + result = _dt_list(obj) - + assert isinstance(result[0], datetime) # Note: _dt_list calls _clean_dict on nested dicts, not _dt_dict # So dates in nested dicts are not parsed @@ -222,9 +221,9 @@ def test_dt_dict_parses_isoformat(): "name": "test", "invalid": "not a date" } - + result = _dt_dict(obj) - + assert isinstance(result["created"], datetime) assert result["created"] == datetime(2025, 1, 1, 12, 0, 0) assert result["name"] == "test" @@ -238,9 +237,9 @@ def test_dt_dict_nested(): "nested": {"date": "2025-01-01"}, "list": ["2025-01-01T10:00:00", 123] } - + result = _dt_dict(obj) - + assert result["name"] == "test" assert isinstance(result["nested"]["date"], datetime) assert isinstance(result["list"][0], datetime) @@ -253,9 +252,9 @@ def test_dt_dict_with_iterable(): "dates": ["2025-01-01", "2025-01-02"], "mixed": ["2025-01-01", "text", 123] } - + result = _dt_dict(obj) - + assert isinstance(result["dates"][0], datetime) assert isinstance(result["dates"][1], datetime) assert isinstance(result["mixed"][0], datetime) @@ -269,9 +268,9 @@ def test_clean_list_handles_dict_items(): {"key1": "value1"}, {"key2": dt} ] - + result = _clean_list(obj) - + assert result[0] == {"key1": "value1"} assert result[1] == {"key2": dt.isoformat()} @@ -282,9 +281,9 @@ def test_dt_list_handles_dict_items(): {"date": "2025-01-01"}, {"name": "test"} ] - + result = _dt_list(obj) - + # _dt_list calls _clean_dict on dict items assert result[0] == {"date": "2025-01-01"} assert result[1] == {"name": "test"} diff --git a/terminusdb_client/tests/test_woqldataframe.py b/terminusdb_client/tests/test_woqldataframe.py index 029b0da3..2dd8ae10 100644 --- a/terminusdb_client/tests/test_woqldataframe.py +++ b/terminusdb_client/tests/test_woqldataframe.py @@ -1,6 +1,6 @@ """Tests for woqldataframe/woqlDataframe.py module.""" import pytest -from unittest.mock import Mock, MagicMock, patch +from unittest.mock import MagicMock, patch from terminusdb_client.woqldataframe.woqlDataframe import result_to_df from terminusdb_client.errors import InterfaceError @@ -9,10 +9,10 @@ def test_result_to_df_requires_pandas(): """Test that result_to_df raises ImportError when pandas is not available.""" with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module') as mock_import: mock_import.side_effect = ImportError("No module named 'pandas'") - + with pytest.raises(ImportError) as exc_info: result_to_df([{"@id": "test", "@type": "Test"}]) - + assert "pandas" in str(exc_info.value).lower() assert "terminus-client-python[dataframe]" in str(exc_info.value) @@ -21,30 +21,30 @@ def test_result_to_df_requires_client_with_max_embed(): """Test that result_to_df raises ValueError when max_embed_dep > 0 without client.""" mock_pd = MagicMock() mock_pd.DataFrame.return_value.from_records.return_value = mock_pd.DataFrame.return_value - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): with pytest.raises(ValueError) as exc_info: result_to_df([{"@id": "test", "@type": "Test"}], max_embed_dep=1) - + assert "client need to be provide" in str(exc_info.value) def test_result_to_df_multiple_types_error(): """Test that result_to_df raises ValueError for multiple document types.""" mock_pd = MagicMock() - + # Create mock DataFrame with multiple types mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["Type1", "Type2"] mock_pd.DataFrame.return_value.from_records.return_value = mock_df - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): with pytest.raises(ValueError) as exc_info: result_to_df([ {"@id": "test1", "@type": "Type1"}, {"@id": "test2", "@type": "Type2"} ]) - + assert "multiple type" in str(exc_info.value).lower() @@ -52,18 +52,18 @@ def test_result_to_df_class_not_in_schema(): """Test that result_to_df raises InterfaceError when class not found in schema.""" mock_pd = MagicMock() mock_client = MagicMock() - + # Setup mock DataFrame mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["UnknownClass"] mock_df.columns = ["@id", "@type"] mock_df.rename.return_value = mock_df mock_df.drop.return_value = mock_df - + mock_pd.DataFrame.return_value.from_records.return_value = mock_df mock_client.get_existing_classes.return_value = {"KnownClass": {}} mock_client.db = "testdb" - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): with pytest.raises(InterfaceError) as exc_info: result_to_df( @@ -71,7 +71,7 @@ def test_result_to_df_class_not_in_schema(): max_embed_dep=1, client=mock_client ) - + assert "UnknownClass" in str(exc_info.value) assert "not found" in str(exc_info.value) @@ -79,21 +79,21 @@ def test_result_to_df_class_not_in_schema(): def test_result_to_df_basic_conversion(): """Test basic result_to_df conversion without embedding.""" mock_pd = MagicMock() - + # Setup mock DataFrame mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["Person"] mock_df.columns = ["@id", "@type", "name"] mock_df.rename.return_value = mock_df mock_df.drop.return_value = mock_df - + mock_pd.DataFrame.return_value.from_records.return_value = mock_df - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): result = result_to_df([ {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} ]) - + # Should return the DataFrame assert result is not None @@ -101,19 +101,19 @@ def test_result_to_df_basic_conversion(): def test_result_to_df_with_keepid(): """Test result_to_df with keepid=True.""" mock_pd = MagicMock() - + # Setup mock DataFrame mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["Person"] mock_df.columns = ["@id", "@type", "name"] - + mock_pd.DataFrame.return_value.from_records.return_value = mock_df - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): result = result_to_df([ {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} ], keepid=True) - + # Should return the DataFrame assert result is not None # Should not call rename when keepid=True @@ -123,12 +123,12 @@ def test_result_to_df_with_keepid(): def test_result_to_df_requires_client_for_embedding(): """Test that result_to_df requires client when max_embed_dep > 0.""" mock_pd = MagicMock() - + # Setup basic mock mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["Person"] mock_pd.DataFrame.return_value.from_records.return_value = mock_df - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): # This tests the ValueError raised at line 18-21 with pytest.raises(ValueError) as exc_info: @@ -137,14 +137,14 @@ def test_result_to_df_requires_client_for_embedding(): max_embed_dep=2, # Requires client client=None # But no client provided ) - + assert "client need to be provide" in str(exc_info.value) def test_result_to_df_expand_nested_json(): """Test that result_to_df expands nested JSON structures.""" mock_pd = MagicMock() - + # Setup mock DataFrame with nested structure mock_df = MagicMock() mock_df.__getitem__.return_value.unique.return_value = ["Person"] @@ -152,14 +152,14 @@ def test_result_to_df_expand_nested_json(): mock_df.rename.return_value = mock_df mock_df.drop.return_value = mock_df mock_df.join.return_value = mock_df - + # Mock json_normalize to simulate expansion mock_expanded = MagicMock() mock_expanded.columns = ["@id", "street"] mock_pd.json_normalize.return_value = mock_expanded - + mock_pd.DataFrame.return_value.from_records.return_value = mock_df - + with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): result = result_to_df([ { @@ -168,7 +168,7 @@ def test_result_to_df_expand_nested_json(): "address": {"@id": "Address/1", "street": "Main St"} } ]) - + # json_normalize should be called for expansion assert mock_pd.json_normalize.called assert result is not None