Skip to content

Commit bbc6100

Browse files
authored
Merge branch 'main' into select-with-no-variables
2 parents f6ea8d4 + 62abdb8 commit bbc6100

11 files changed

Lines changed: 237 additions & 50 deletions

File tree

.github/workflows/python.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,3 @@ jobs:
7070
7171
- name: Deploy to pypi
7272
uses: pypa/gh-action-pypi-publish@release/v1
73-
with:
74-
user: __token__
75-
password: ${{ secrets.PYPI_API_TOKEN }}

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/TerminusDB?style=social)](https://www.reddit.com/r/TerminusDB/)
99
[![Twitter](https://img.shields.io/twitter/follow/terminusdb?color=skyblue&label=Follow%20on%20Twitter&logo=twitter&style=flat)](https://twitter.com/TerminusDB)
1010

11-
[![release version](https://img.shields.io/pypi/v/terminusdb-client.svg?logo=pypi)](https://pypi.python.org/pypi/terminusdb-client/)
12-
[![downloads](https://img.shields.io/pypi/dm/terminusdb-client.svg?logo=pypi)](https://pypi.python.org/pypi/terminusdb-client/)
11+
[![release version](https://img.shields.io/pypi/v/terminusdb.svg?logo=pypi)](https://pypi.python.org/pypi/terminusdb/)
12+
[![downloads](https://img.shields.io/pypi/dm/terminusdb.svg?logo=pypi)](https://pypi.python.org/pypi/terminusdb/)
1313

1414
[![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)
1515
[![documentation](https://img.shields.io/github/deployments/terminusdb/terminusdb-client-python/github-pages?label=documentation&logo=github)](https://terminusdb.org/docs/python)
@@ -18,6 +18,10 @@
1818

1919
> Python client for TerminusDB and TerminusCMS.
2020
21+
> **Migrating from `terminusdb-client`?** This package was formerly known as
22+
> `terminusdb-client`. Simply install `terminusdb` instead — both `import terminusdb`
23+
> and `import terminusdb_client` continue to work, so no code changes are required.
24+
2125
[**TerminusDB**][terminusdb] is an [open-source][terminusdb-repo] graph database
2226
and document store. It allows you to link JSON documents in a powerful knowledge
2327
graph all through a simple document API, with full git-for-data version control.
@@ -28,26 +32,26 @@ graph all through a simple document API, with full git-for-data version control.
2832

2933
## Requirements
3034

31-
- [TerminusDB v11.1](https://github.com/terminusdb/terminusdb-server)
35+
- [TerminusDB v12](https://github.com/terminusdb/terminusdb-server)
3236
- [Python >=3.9](https://www.python.org/downloads)
3337

3438
## Release Notes and Previous Versions
3539

36-
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.
40+
TerminusDB Client v12 works with TerminusDB v12 onwards and the [DFRNT cloud service](https://dfrnt.com). Please check the [Release Notes](RELEASE_NOTES.md) to find out what has changed.
3741

3842
## Installation
3943
- TerminusDB Client can be downloaded from PyPI using pip:
40-
`python -m pip install terminusdb-client`
44+
`python -m pip install terminusdb`
4145

4246
This only includes the core Python Client (Client) and WOQLQuery.
4347

4448
If you want to use woqlDataframe or the import and export CSV function in the Scaffolding CLI tool:
4549

46-
`python -m pip install terminusdb-client[dataframe]`
50+
`python -m pip install terminusdb[dataframe]`
4751

4852
*if you are installing from `zsh` you have to quote the argument like this:*
4953

50-
`python -m pip install 'terminusdb-client[dataframe]'`
54+
`python -m pip install 'terminusdb[dataframe]'`
5155

5256
- Install from source:
5357

@@ -66,19 +70,21 @@ If you want to use woqlDataframe or the import and export CSV function in the Sc
6670
Connect to local host
6771

6872
```Python
69-
from terminusdb_client import Client
73+
from terminusdb import Client
7074

7175
client = Client("http://127.0.0.1:6363/")
7276
client.connect()
7377
```
7478

79+
The previous import path `from terminusdb_client import Client` also continues to work.
80+
7581
Connect to TerminusDB in the cloud
7682

7783
*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*
7884

7985

8086
```Python
81-
from terminusdb_client import Client
87+
from terminusdb import Client
8288

8389
team="MyTeam"
8490
client = Client(f"https://studio.dfrnt.com/api/hosted/{team}/")
@@ -94,7 +100,7 @@ client.create_database("MyDatabase")
94100
#### Create a schema
95101

96102
```Python
97-
from terminusdb_client.schema import Schema, DocumentTemplate, RandomKey
103+
from terminusdb.schema import Schema, DocumentTemplate, RandomKey
98104

99105
my_schema = Schema()
100106

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
[tool.poetry]
2-
name = "terminusdb-client"
2+
name = "terminusdb"
33
version = "12.0.3"
4-
description = "Python client for Terminus DB"
5-
authors = ["TerminusDB group"]
4+
description = "Terminus DB Python client"
5+
authors = ["TerminusDB group", "DFRNT AB"]
66
license = "Apache Software License"
77
readme = "README.md"
8+
packages = [
9+
{include = "terminusdb_client"},
10+
{include = "terminusdb"},
11+
]
812

913
[tool.poetry.dependencies]
1014
python = ">=3.9.0,<3.13"

terminusdb/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Re-export everything from terminusdb_client for the new import path.
2+
# Both `import terminusdb` and `import terminusdb_client` are supported.
3+
from terminusdb_client import * # noqa
4+
from terminusdb_client import Client, WOQLClient, WOQLQuery, Var, Vars, Patch, GraphType, WOQLDataFrame, WOQLSchema # noqa

terminusdb/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from terminusdb_client.client import * # noqa
2+
from terminusdb_client.client import Client, GraphType, Patch # noqa
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from terminusdb_client.query_syntax import * # noqa

terminusdb/schema/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from terminusdb_client.schema import * # noqa

terminusdb_client/scripts/scripts.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,6 @@ def importcsv(
486486
embedded = [x.lower().replace(" ", "_") for x in embedded]
487487
try:
488488
pd = import_module("pandas")
489-
np = import_module("numpy")
490489
except ImportError:
491490
raise ImportError(
492491
"Library 'pandas' is required to import csv, either install 'pandas' or install woqlDataframe requirements as follows: python -m pip install -U terminus-client-python[dataframe]"
@@ -516,6 +515,23 @@ def _df_to_schema(class_name, df):
516515
converted_type = np_to_buildin[dtype.type]
517516
if converted_type is object:
518517
converted_type = str # pandas treats all string as objects
518+
# Map pandas/numpy dtype to Python type
519+
# Uses dtype.kind for compatibility with numpy 2.0+ and pandas 3.0+
520+
dtype_kind = getattr(dtype, "kind", "O")
521+
if dtype.type is str or dtype_kind in ("U", "O", "S", "T"):
522+
converted_type = str
523+
elif dtype_kind in ("i", "u"):
524+
converted_type = int
525+
elif dtype_kind == "f":
526+
converted_type = float
527+
elif dtype_kind == "b":
528+
converted_type = bool
529+
elif dtype_kind == "M":
530+
converted_type = dt.datetime
531+
elif dtype_kind == "m":
532+
converted_type = dt.timedelta
533+
else:
534+
converted_type = str
519535
converted_type = wt.to_woql_type(converted_type)
520536

521537
if id_ and col == id_:
@@ -547,15 +563,7 @@ def _df_to_schema(class_name, df):
547563
converted_col = col.lower().replace(" ", "_").replace(".", "_")
548564
df.rename(columns={col: converted_col}, inplace=True)
549565
if not has_schema:
550-
class_dict = _df_to_schema(
551-
class_name,
552-
df,
553-
np,
554-
embedded=embedded,
555-
id_col=id_,
556-
na_mode=na,
557-
keys=keys,
558-
)
566+
class_dict = _df_to_schema(class_name, df)
559567
if message is None:
560568
schema_msg = f"Schema object insert/ update with {csv_file} by Python client."
561569
else:

terminusdb_client/tests/integration_tests/test_conftest.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,18 @@ class TestServerDetection:
1414
"""Test server detection helper functions"""
1515

1616
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
17-
def test_local_server_running_200(self, mock_get):
18-
"""Test local server detection returns True for HTTP 200"""
19-
mock_response = Mock()
20-
mock_response.status_code = 200
21-
mock_get.return_value = mock_response
17+
def test_local_server_running_any_response(self, mock_get):
18+
"""Test local server detection returns True for any HTTP response"""
19+
mock_get.return_value = Mock()
2220

2321
assert is_local_server_running() is True
2422
mock_get.assert_called_once_with("http://127.0.0.1:6363/api/", timeout=2)
2523

2624
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
27-
def test_local_server_running_404(self, mock_get):
28-
"""Test local server detection returns True for HTTP 404"""
25+
def test_local_server_running_401(self, mock_get):
26+
"""Test local server detection returns True for HTTP 401 (unauthorized)"""
2927
mock_response = Mock()
30-
mock_response.status_code = 404
28+
mock_response.status_code = 401
3129
mock_get.return_value = mock_response
3230

3331
assert is_local_server_running() is True
@@ -47,20 +45,18 @@ def test_local_server_not_running_timeout(self, mock_get):
4745
assert is_local_server_running() is False
4846

4947
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
50-
def test_docker_server_running_200(self, mock_get):
51-
"""Test Docker server detection returns True for HTTP 200"""
52-
mock_response = Mock()
53-
mock_response.status_code = 200
54-
mock_get.return_value = mock_response
48+
def test_docker_server_running_any_response(self, mock_get):
49+
"""Test Docker server detection returns True for any HTTP response"""
50+
mock_get.return_value = Mock()
5551

5652
assert is_docker_server_running() is True
5753
mock_get.assert_called_once_with("http://127.0.0.1:6366/api/", timeout=2)
5854

5955
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
60-
def test_docker_server_running_404(self, mock_get):
61-
"""Test Docker server detection returns True for HTTP 404"""
56+
def test_docker_server_running_401(self, mock_get):
57+
"""Test Docker server detection returns True for HTTP 401 (unauthorized)"""
6258
mock_response = Mock()
63-
mock_response.status_code = 404
59+
mock_response.status_code = 401
6460
mock_get.return_value = mock_response
6561

6662
assert is_docker_server_running() is True
@@ -73,20 +69,18 @@ def test_docker_server_not_running(self, mock_get):
7369
assert is_docker_server_running() is False
7470

7571
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
76-
def test_jwt_server_running_200(self, mock_get):
77-
"""Test JWT server detection returns True for HTTP 200"""
78-
mock_response = Mock()
79-
mock_response.status_code = 200
80-
mock_get.return_value = mock_response
72+
def test_jwt_server_running_any_response(self, mock_get):
73+
"""Test JWT server detection returns True for any HTTP response"""
74+
mock_get.return_value = Mock()
8175

8276
assert is_jwt_server_running() is True
8377
mock_get.assert_called_once_with("http://127.0.0.1:6367/api/", timeout=2)
8478

8579
@patch("terminusdb_client.tests.integration_tests.conftest.requests.get")
86-
def test_jwt_server_running_404(self, mock_get):
87-
"""Test JWT server detection returns True for HTTP 404"""
80+
def test_jwt_server_running_401(self, mock_get):
81+
"""Test JWT server detection returns True for HTTP 401 (unauthorized)"""
8882
mock_response = Mock()
89-
mock_response.status_code = 404
83+
mock_response.status_code = 401
9084
mock_get.return_value = mock_response
9185

9286
assert is_jwt_server_running() is True
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Integration tests for WOQL Collect predicate.
3+
4+
Collect gathers all solutions from a sub-query into a list,
5+
completing the list/binding symmetry alongside Member:
6+
- Member: List -> Bindings (destructure)
7+
- Collect: Bindings -> List (gather)
8+
"""
9+
10+
import pytest
11+
12+
from terminusdb_client import Client
13+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
14+
15+
test_user_agent = "terminusdb-client-python-tests"
16+
17+
18+
def extract_values(result_list):
19+
"""Extract raw values from a list of typed literals."""
20+
if not result_list:
21+
return []
22+
return [
23+
item["@value"] if isinstance(item, dict) and "@value" in item else item
24+
for item in result_list
25+
]
26+
27+
28+
class TestWOQLCollect:
29+
"""Tests for the WOQL Collect predicate."""
30+
31+
@pytest.fixture(autouse=True)
32+
def setup_teardown(self, docker_url):
33+
"""Setup and teardown for each test."""
34+
self.client = Client(docker_url, user_agent=test_user_agent)
35+
self.client.connect()
36+
self.db_name = "test_woql_collect"
37+
38+
# Create database for tests
39+
if self.db_name in self.client.list_databases():
40+
self.client.delete_database(self.db_name)
41+
self.client.create_database(self.db_name)
42+
43+
# Add schema
44+
self.client.insert_document(
45+
[
46+
{
47+
"@type": "@context",
48+
"@base": "terminusdb:///data/",
49+
"@schema": "terminusdb:///schema#",
50+
},
51+
{
52+
"@id": "NamedThing",
53+
"@type": "Class",
54+
"@key": {"@type": "Lexical", "@fields": ["name"]},
55+
"name": "xsd:string",
56+
},
57+
],
58+
graph_type="schema",
59+
full_replace=True,
60+
)
61+
62+
# Insert test documents
63+
self.client.insert_document(
64+
[
65+
{"@type": "NamedThing", "name": "Alice"},
66+
{"@type": "NamedThing", "name": "Bob"},
67+
{"@type": "NamedThing", "name": "Carol"},
68+
]
69+
)
70+
71+
yield
72+
73+
# Cleanup
74+
self.client.delete_database(self.db_name)
75+
76+
def test_collect_triple_objects_into_list(self):
77+
"""Collect gathers all matching triple objects into a single list."""
78+
query = WOQLQuery().collect(
79+
"v:name",
80+
"v:names",
81+
WOQLQuery().triple("v:doc", "name", "v:name"),
82+
)
83+
84+
result = self.client.query(query)
85+
assert len(result["bindings"]) == 1
86+
names = sorted(extract_values(result["bindings"][0]["names"]))
87+
assert names == ["Alice", "Bob", "Carol"]
88+
89+
def test_collect_empty_result(self):
90+
"""Collect produces empty list when sub-query has no solutions."""
91+
query = WOQLQuery().collect(
92+
"v:x",
93+
"v:collected",
94+
WOQLQuery().triple("v:doc", "nonexistent_property", "v:x"),
95+
)
96+
97+
result = self.client.query(query)
98+
assert len(result["bindings"]) == 1
99+
assert result["bindings"][0]["collected"] == []
100+
101+
def test_collect_composes_with_length(self):
102+
"""Collect result can be used with length to count solutions."""
103+
query = WOQLQuery().woql_and(
104+
WOQLQuery().collect(
105+
"v:name",
106+
"v:names",
107+
WOQLQuery().triple("v:doc", "name", "v:name"),
108+
),
109+
WOQLQuery().length("v:names", "v:count"),
110+
)
111+
112+
result = self.client.query(query)
113+
assert len(result["bindings"]) == 1
114+
assert result["bindings"][0]["count"]["@value"] == 3
115+
116+
def test_collect_with_limit_in_subquery(self):
117+
"""Collect respects limit inside the sub-query."""
118+
query = WOQLQuery().collect(
119+
"v:name",
120+
"v:names",
121+
WOQLQuery().limit(2, WOQLQuery().triple("v:doc", "name", "v:name")),
122+
)
123+
124+
result = self.client.query(query)
125+
assert len(result["bindings"]) == 1
126+
assert len(result["bindings"][0]["names"]) == 2
127+
128+
def test_collect_with_list_template(self):
129+
"""Collect with multi-element list template produces nested lists."""
130+
query = WOQLQuery().collect(
131+
["v:doc", "v:name"],
132+
"v:pairs",
133+
WOQLQuery().triple("v:doc", "name", "v:name"),
134+
)
135+
136+
result = self.client.query(query)
137+
assert len(result["bindings"]) == 1
138+
pairs = result["bindings"][0]["pairs"]
139+
assert len(pairs) == 3
140+
for pair in pairs:
141+
assert isinstance(pair, list)
142+
assert len(pair) == 2

0 commit comments

Comments
 (0)