Skip to content

Commit 48b2a09

Browse files
authored
Merge branch 'main' into support-for-prefixes
2 parents c0e9a33 + 9f69b3d commit 48b2a09

File tree

11 files changed

+824
-45
lines changed

11 files changed

+824
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ terminusdb_client_coverage/
4040
*~
4141

4242
venv/
43+
.venv/
4344

4445
# due to using tox and pytest
4546
.tox

CONTRIBUTING.md

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,40 +44,59 @@ That's it! You're now ready to start contributing. See [Poetry Scripts Reference
4444

4545
## Setting up dev environment 💻
4646

47-
Make sure you have Python>=3.9 installed. We use [Poetry](https://python-poetry.org/) for dependency management.
47+
Make sure you have Python>=3.9 and <3.13 installed.
4848

49-
### Using Poetry (Recommended)
49+
[Fork and clone](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repo, then set up your development environment using one of the methods below.
5050

51-
1. [Fork and clone](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repo
51+
### Option 1: Using venv (recommended)
5252

53-
2. Create and activate a virtual environment:
54-
```bash
55-
python3 -m venv .venv
56-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
57-
```
53+
Create and activate a virtual environment:
5854

59-
3. Install Poetry and dependencies:
60-
```bash
61-
pip install poetry
62-
poetry install --with dev
63-
```
55+
```bash
56+
# Create venv with Python 3.12 (or any version 3.9-3.12)
57+
python3.12 -m venv .venv
6458

65-
4. Install the package in editable mode:
66-
```bash
67-
poetry run dev install-dev
68-
```
59+
# Activate the virtual environment
60+
source .venv/bin/activate # On macOS/Linux
61+
# .venv\Scripts\activate # On Windows
6962

70-
5. Start TerminusDB server (required for integration tests):
71-
```bash
72-
docker run --pull always -d -p 127.0.0.1:6363:6363 -v terminusdb_storage:/app/terminusdb/storage --name terminusdb terminusdb/terminusdb-server:v12
73-
```
74-
75-
To stop the server when done:
63+
# Install the package in editable mode with dev dependencies
64+
pip install -e ".[dev]"
65+
66+
# Install pytest for running tests
67+
pip install pytest
68+
```
69+
70+
### Option 2: Using pipenv
71+
72+
We also support [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) for dev environment:
73+
74+
```bash
75+
pip install pipenv --upgrade
76+
pipenv install --dev --pre
77+
```
78+
79+
Or simply run `make init`.
80+
81+
To "editable" install the local Terminus Client Python:
82+
83+
`pip install -e .`
84+
85+
### Running a local TerminusDB server
86+
87+
**To run integration tests, you need either Docker or a local TerminusDB server.**
88+
89+
For integration tests, you can either:
90+
91+
1. **Use Docker** (automatic): Tests will automatically start a Docker container if no server is detected
92+
2. **Use a local server**: Start the TerminusDB test server from the main terminusdb repository:
7693
```bash
77-
docker stop terminusdb
78-
docker rm terminusdb
94+
cd /path/to/terminusdb
95+
./tests/terminusdb-test-server.sh start
7996
```
8097

98+
The test configuration will automatically detect and use an available server.
99+
81100
**To run integration tests, Docker must be installed locally.**
82101

83102
We use [shed](https://pypi.org/project/shed/) to lint our code. You can run it manually with `poetry run dev lint`, or set up a pre-commit hook (requires Python 3.10+):

terminusdb_client/client/Client.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,108 @@ def get_commit_history(self, max_history: int = 500) -> list:
563563
raise ValueError("max_history needs to be non-negative.")
564564
return self.log(count=max_history)
565565

566+
def get_document_history(
567+
self,
568+
doc_id: str,
569+
team: Optional[str] = None,
570+
db: Optional[str] = None,
571+
start: int = 0,
572+
count: int = 10,
573+
created: bool = False,
574+
updated: bool = False,
575+
) -> list:
576+
"""Get the commit history for a specific document
577+
578+
Returns the history of changes made to a document, ordered backwards
579+
in time from the most recent change. Only commits where the specified
580+
document was created, modified, or deleted are included.
581+
582+
Parameters
583+
----------
584+
doc_id : str
585+
The document ID (IRI) to retrieve history for (e.g., "Person/alice")
586+
team : str, optional
587+
The team from which the database is. Defaults to the class property.
588+
db : str, optional
589+
The database. Defaults to the class property.
590+
start : int, optional
591+
Starting index for pagination. Defaults to 0.
592+
count : int, optional
593+
Maximum number of history entries to return. Defaults to 10.
594+
created : bool, optional
595+
If True, return only the creation time. Defaults to False.
596+
updated : bool, optional
597+
If True, return only the last update time. Defaults to False.
598+
599+
Raises
600+
------
601+
InterfaceError
602+
If the client is not connected to a database
603+
DatabaseError
604+
If the API request fails or document is not found
605+
606+
Returns
607+
-------
608+
list
609+
List of history entry dictionaries containing commit information
610+
for the specified document:
611+
```
612+
[
613+
{
614+
"author": "admin",
615+
"identifier": "tbn15yq6rw1l4e9bgboyu3vwcoxgri5",
616+
"message": "Updated document",
617+
"timestamp": datetime.datetime(2023, 4, 6, 19, 1, 14, 324928)
618+
},
619+
{
620+
"author": "admin",
621+
"identifier": "3v3naa8jrt8612dg5zryu4vjqwa2w9s",
622+
"message": "Created document",
623+
"timestamp": datetime.datetime(2023, 4, 6, 19, 0, 47, 406387)
624+
}
625+
]
626+
```
627+
628+
Example
629+
-------
630+
>>> from terminusdb_client import Client
631+
>>> client = Client("http://127.0.0.1:6363")
632+
>>> client.connect(db="example_db")
633+
>>> history = client.get_document_history("Person/Jane")
634+
>>> print(f"Document modified {len(history)} times")
635+
>>> print(f"Last change by: {history[0]['author']}")
636+
"""
637+
self._check_connection(check_db=(not team or not db))
638+
team = team if team else self.team
639+
db = db if db else self.db
640+
641+
params = {
642+
'id': doc_id,
643+
'start': start,
644+
'count': count,
645+
}
646+
if created:
647+
params['created'] = created
648+
if updated:
649+
params['updated'] = updated
650+
651+
result = self._session.get(
652+
f"{self.api}/history/{team}/{db}",
653+
params=params,
654+
headers=self._default_headers,
655+
auth=self._auth(),
656+
)
657+
658+
history = json.loads(_finish_response(result))
659+
660+
# Post-process timestamps from Unix timestamp to datetime objects
661+
if isinstance(history, list):
662+
for entry in history:
663+
if 'timestamp' in entry and isinstance(entry['timestamp'], (int, float)):
664+
entry['timestamp'] = datetime.fromtimestamp(entry['timestamp'])
665+
666+
return history
667+
566668
def _get_current_commit(self):
567669
descriptor = self.db
568670
if self.branch:

terminusdb_client/tests/integration_tests/conftest.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def is_local_server_running():
1313
"""Check if local TerminusDB server is running at http://127.0.0.1:6363"""
1414
try:
1515
response = requests.get("http://127.0.0.1:6363", timeout=2)
16-
# Server responds with 404 for root path, which means it's running
16+
# Server responds with 200 (success) or 404 (not found but server is up)
17+
# 401 (unauthorized) also indicates server is running but needs auth
1718
return response.status_code in [200, 404]
1819
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
1920
return False
@@ -73,7 +74,9 @@ def docker_url_jwt(pytestconfig):
7374

7475
# Check if JWT server is already running (port 6367)
7576
if is_jwt_server_running():
76-
print("\n✓ Using existing JWT Docker TerminusDB server at http://127.0.0.1:6367")
77+
print(
78+
"\n✓ Using existing JWT Docker TerminusDB server at http://127.0.0.1:6367"
79+
)
7780
yield ("http://127.0.0.1:6367", jwt_token)
7881
return # Don't clean up - server was already running
7982

@@ -128,7 +131,9 @@ def docker_url_jwt(pytestconfig):
128131

129132
if seconds_waited > MAX_CONTAINER_STARTUP_TIME:
130133
clean_up_container()
131-
raise RuntimeError(f"JWT Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)")
134+
raise RuntimeError(
135+
f"JWT Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)"
136+
)
132137

133138
yield (test_url, jwt_token)
134139
clean_up_container()
@@ -145,9 +150,15 @@ def docker_url(pytestconfig):
145150
"""
146151
# Check if local test server is already running (port 6363)
147152
if is_local_server_running():
148-
print("\n✓ Using existing local TerminusDB test server at http://127.0.0.1:6363")
149-
print("⚠️ WARNING: Local server should be started with TERMINUSDB_AUTOLOGIN=true")
150-
print(" Or use: TERMINUSDB_SERVER_AUTOLOGIN=true ./tests/terminusdb-test-server.sh restart")
153+
print(
154+
"\n✓ Using existing local TerminusDB test server at http://127.0.0.1:6363"
155+
)
156+
print(
157+
"⚠️ WARNING: Local server should be started with TERMINUSDB_AUTOLOGIN=true"
158+
)
159+
print(
160+
" Or use: TERMINUSDB_SERVER_AUTOLOGIN=true ./tests/terminusdb-test-server.sh restart"
161+
)
151162
yield "http://127.0.0.1:6363"
152163
return # Don't clean up - server was already running
153164

@@ -200,7 +211,9 @@ def docker_url(pytestconfig):
200211
response = requests.get(test_url)
201212
# Server responds with 404 for root path, which means it's running
202213
assert response.status_code in [200, 404]
203-
print(f"✓ Docker container started successfully after {seconds_waited}s")
214+
print(
215+
f"✓ Docker container started successfully after {seconds_waited}s"
216+
)
204217
break
205218
except (requests.exceptions.ConnectionError, AssertionError):
206219
pass
@@ -210,7 +223,9 @@ def docker_url(pytestconfig):
210223

211224
if seconds_waited > MAX_CONTAINER_STARTUP_TIME:
212225
clean_up_container()
213-
raise RuntimeError(f"Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)")
226+
raise RuntimeError(
227+
f"Container was too slow to startup (waited {MAX_CONTAINER_STARTUP_TIME}s)"
228+
)
214229

215230
yield test_url
216231
clean_up_container()

terminusdb_client/tests/integration_tests/test_client.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,70 @@ def test_log(docker_url):
211211
assert log[0]['@type'] == 'InitialCommit'
212212

213213

214+
def test_get_document_history(docker_url):
215+
# Create client
216+
client = Client(docker_url, user_agent=test_user_agent)
217+
client.connect()
218+
219+
# Create test database
220+
db_name = "testDB" + str(random())
221+
client.create_database(db_name, team="admin")
222+
client.connect(db=db_name)
223+
224+
# Add a schema
225+
schema = {
226+
"@type": "Class",
227+
"@id": "Person",
228+
"name": "xsd:string",
229+
"age": "xsd:integer"
230+
}
231+
client.insert_document(schema, graph_type=GraphType.SCHEMA)
232+
233+
# Insert a document
234+
person = {"@type": "Person", "@id": "Person/Jane", "name": "Jane", "age": 30}
235+
client.insert_document(person, commit_msg="Created Person/Jane")
236+
237+
# Update the document to create history
238+
person["name"] = "Jane Doe"
239+
person["age"] = 31
240+
client.update_document(person, commit_msg="Updated Person/Jane name and age")
241+
242+
# Update again
243+
person["age"] = 32
244+
client.update_document(person, commit_msg="Updated Person/Jane age")
245+
246+
# Get document history
247+
history = client.get_document_history("Person/Jane")
248+
249+
# Assertions
250+
assert isinstance(history, list)
251+
assert len(history) >= 3 # At least insert and two updates
252+
assert all('timestamp' in entry for entry in history)
253+
assert all(isinstance(entry['timestamp'], dt.datetime) for entry in history)
254+
assert all('author' in entry for entry in history)
255+
assert all('message' in entry for entry in history)
256+
assert all('identifier' in entry for entry in history)
257+
258+
# Verify messages are in the history (order may vary)
259+
messages = [entry['message'] for entry in history]
260+
assert "Created Person/Jane" in messages
261+
assert "Updated Person/Jane name and age" in messages
262+
assert "Updated Person/Jane age" in messages
263+
264+
# Test with pagination
265+
paginated_history = client.get_document_history("Person/Jane", start=0, count=2)
266+
assert len(paginated_history) == 2
267+
268+
# Test with team/db override
269+
history_override = client.get_document_history(
270+
"Person/Jane", team="admin", db=db_name
271+
)
272+
assert len(history_override) == len(history)
273+
274+
# Cleanup
275+
client.delete_database(db_name, "admin")
276+
277+
214278
def test_get_triples(docker_url):
215279
client = Client(docker_url, user_agent=test_user_agent, team="admin")
216280
client.connect()

0 commit comments

Comments
 (0)