Skip to content

Commit abc30a4

Browse files
committed
Add client-side database sync infrastructure (#418)
Add openlifu.cloud module for bidirectional sync between local OpenLIFU databases and the cloud API (api.openwater.health). Introduces a REST API client layer with typed DTOs for all entity types (subjects, sessions, protocols, systems, transducers, volumes, runs, solutions, users, photoscans). Sync engine uses a component tree (AbstractComponent) with per-entity-type subclasses, a debounced worker thread (SyncThread), filesystem watching via watchdog, and WebSocket-based push notifications for real-time cloud-to-local updates. Volumes and photoscans are download-only (pulled from cloud then deleted server-side); all other entities sync bidirectionally. Adds watchdog and python-socketio[client] dependencies. Squashed commit of the following: commit c11402d Author: Pavel Osnova <pvosnova@gmail.com> Date: Mon Feb 23 10:43:55 2026 +0200 fix: endless sync loop commit 3894d67 Author: dev-et <dev-et> Date: Fri Feb 20 18:15:26 2026 -0800 Set daemon to false commit 565ef1c Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 20 18:12:35 2026 +0200 fix: don't delete photoscans on cloud commit 871188e Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 20 13:10:32 2026 +0200 fix: download only finished photoscans commit 7f490c2 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 20 12:51:23 2026 +0200 fix: don't sync photocollections and don't upload volumes to cloud commit 54cb278 Author: dev-et <dev-et> Date: Thu Feb 19 21:59:55 2026 -0800 Disable daemon for sync thread commit 6e91aa4 Author: Pavel Osnova <pvosnova@gmail.com> Date: Thu Feb 19 15:36:55 2026 +0200 fix: getting MAC address on Windows commit f3c104e Author: Pavel Osnova <pvosnova@gmail.com> Date: Thu Feb 19 10:27:20 2026 +0200 fix: disable photocollections sync commit 63c2900 Author: dev-et <dev-et> Date: Wed Feb 18 10:08:06 2026 -0800 Add temporary hardcoded mac address commit 2b07ec9 Author: Pavel Osnova <pvosnova@gmail.com> Date: Wed Feb 18 13:57:25 2026 +0200 fix: resolve lint errors commit 9d41092 Author: Pavel Osnova <pvosnova@gmail.com> Date: Wed Feb 18 13:17:29 2026 +0200 fix: resolve lint errors commit 1edb8a5 Author: dev-et <dev-et> Date: Mon Feb 16 21:03:25 2026 -0800 Add exception handling for errorous files commit 7d4e84b Author: dev-et <dev-et> Date: Mon Feb 16 01:37:06 2026 -0800 Bug fixes, add additional logging, fix deadlock in sync_thread commit d8f2894 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 13 15:19:33 2026 +0200 One way sync for volumes, photocollections and photoscans and delete them on cloud after sync. commit 174ba08 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 13 12:07:48 2026 +0200 Fixes photocollection sync when photo added locally commit 366ef47 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 13 11:50:10 2026 +0200 Don't block the main thread when stopping the cloud commit a28c622 Author: Pavel Osnova <pvosnova@gmail.com> Date: Thu Feb 12 13:34:54 2026 +0200 Fixes sync error commit 20da259 Author: dev-et <dev-et> Date: Mon Feb 9 13:09:39 2026 -0800 Fix NameError: name 'AbstractComponent' is not defined exception commit 08731b8 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 6 16:51:15 2026 +0200 Don't delete database owner automatically when sync library stops commit 20d7a48 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 6 15:10:06 2026 +0200 Photoscans sync commit 253dc32 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Feb 6 12:47:46 2026 +0200 Synchronization fixes commit d84c2d0 Author: Pavel Osnova <pvosnova@gmail.com> Date: Thu Feb 5 18:25:17 2026 +0200 Synchronization of sessions, volumes, photocollections, runs and solutions commit 75103b3 Author: Pavel Osnova <pvosnova@gmail.com> Date: Mon Feb 2 11:17:32 2026 +0200 update API URL commit 8c01179 Author: Pavel Osnova <pvosnova@gmail.com> Date: Fri Jan 30 12:21:45 2026 +0200 initial cloud library
1 parent 4ff3be8 commit abc30a4

36 files changed

Lines changed: 2457 additions & 0 deletions

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ dependencies = [
5151
"embreex; platform_machine=='x86_64' or platform_machine=='AMD64'",
5252
"onnxruntime==1.18.0",
5353
"pydicom",
54+
"watchdog",
55+
"python-socketio[client]"
5456
]
5557

5658
[project.optional-dependencies]

src/openlifu/cloud/__init__.py

Whitespace-only changes.

src/openlifu/cloud/api/__init__.py

Whitespace-only changes.

src/openlifu/cloud/api/api.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
from openlifu.cloud.api.databases_api import DatabasesApi
4+
from openlifu.cloud.api.photoscans_api import PhotoscansApi
5+
from openlifu.cloud.api.protocols_api import ProtocolsApi
6+
from openlifu.cloud.api.request import Request
7+
from openlifu.cloud.api.runs_api import RunsApi
8+
from openlifu.cloud.api.sessions_api import SessionsApi
9+
from openlifu.cloud.api.solutions_api import SolutionsApi
10+
from openlifu.cloud.api.subjects_api import SubjectsApi
11+
from openlifu.cloud.api.systems_api import SystemsApi
12+
from openlifu.cloud.api.transducers_api import TransducersApi
13+
from openlifu.cloud.api.users_api import UsersApi
14+
from openlifu.cloud.api.volumes_api import VolumesApi
15+
16+
17+
class Api:
18+
19+
def __init__(self):
20+
self._request = Request()
21+
self._request.debug_log = True
22+
self._databases = DatabasesApi(self._request)
23+
self._protocols = ProtocolsApi(self._request)
24+
self._users = UsersApi(self._request)
25+
self._systems = SystemsApi(self._request)
26+
self._transducers = TransducersApi(self._request)
27+
self._subjects = SubjectsApi(self._request)
28+
self._volumes = VolumesApi(self._request)
29+
self._sessions = SessionsApi(self._request)
30+
self._runs = RunsApi(self._request)
31+
self._solutions = SolutionsApi(self._request)
32+
self._photoscans = PhotoscansApi(self._request)
33+
34+
def authenticate(self, token: str):
35+
self._request.headers = {
36+
"Authorization": f"Bearer {token}",
37+
"Content-Type": "application/json"
38+
}
39+
40+
def logout(self):
41+
self._request.headers = {}
42+
43+
def databases(self) -> DatabasesApi:
44+
return self._databases
45+
46+
def protocols(self) -> ProtocolsApi:
47+
return self._protocols
48+
49+
def users(self) -> UsersApi:
50+
return self._users
51+
52+
def systems(self) -> SystemsApi:
53+
return self._systems
54+
55+
def transducers(self) -> TransducersApi:
56+
return self._transducers
57+
58+
def subjects(self) -> SubjectsApi:
59+
return self._subjects
60+
61+
def volumes(self) -> VolumesApi:
62+
return self._volumes
63+
64+
def sessions(self) -> SessionsApi:
65+
return self._sessions
66+
67+
def runs(self) -> RunsApi:
68+
return self._runs
69+
70+
def solutions(self) -> SolutionsApi:
71+
return self._solutions
72+
73+
def photoscans(self) -> PhotoscansApi:
74+
return self._photoscans
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from openlifu.cloud.api.dto import ClaimDbDto, DatabaseDto, DatabaseSyncRequestDto
4+
from openlifu.cloud.api.request import Request
5+
from openlifu.cloud.utils import from_json
6+
7+
8+
class DatabasesApi:
9+
10+
def __init__(self, request: Request):
11+
self._request = request
12+
13+
def claim_database(self, dto: ClaimDbDto) -> DatabaseDto:
14+
response = self._request.put("/databases/claim", dto)
15+
return from_json(DatabaseDto, response)
16+
17+
def release_database(self, database_id: int):
18+
self._request.delete(f"/databases/{database_id}/owner")
19+
20+
def get_database(self, database_id: int) -> DatabaseDto:
21+
response = self._request.get(f"/databases/{database_id}")
22+
return from_json(DatabaseDto, response)
23+
24+
def update_database_sync_date(self, database_id: int, dto: DatabaseSyncRequestDto):
25+
self._request.put(f"/databases/{database_id}/sync", dto)

0 commit comments

Comments
 (0)