From a9edd550e0c20818fac92263d3fe5d51d9eae2ed Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 22 Dec 2025 19:57:02 +0530 Subject: [PATCH 01/21] start implement sync client --- objectbox/c.py | 149 +++++++++++++++++++++++++++++++++++++++++ objectbox/store.py | 3 + objectbox/sync.py | 163 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+) create mode 100644 objectbox/sync.py diff --git a/objectbox/c.py b/objectbox/c.py index 03b7aea..6addd61 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -251,6 +251,123 @@ class OBX_query(ctypes.Structure): OBX_query_p = ctypes.POINTER(OBX_query) + +# Sync types +class OBX_sync(ctypes.Structure): + pass + + +OBX_sync_p = ctypes.POINTER(OBX_sync) + + +class OBX_sync_server(ctypes.Structure): + pass + + +OBX_sync_server_p = ctypes.POINTER(OBX_sync_server) + + +class OBXSyncCredentialsType(IntEnum): + NONE = 1 + SHARED_SECRET = 2 # Deprecated, use SHARED_SECRET_SIPPED instead + GOOGLE_AUTH = 3 + SHARED_SECRET_SIPPED = 4 # Uses shared secret to create a hashed credential + OBX_ADMIN_USER = 5 # ObjectBox admin users (username/password) + USER_PASSWORD = 6 # Generic credential type for admin users + JWT_ID = 7 # JSON Web Token (JWT): ID token with user identity + JWT_ACCESS = 8 # JSON Web Token (JWT): access token for resources + JWT_REFRESH = 9 # JSON Web Token (JWT): refresh token + JWT_CUSTOM = 10 # JSON Web Token (JWT): custom token type + + +class OBXRequestUpdatesMode(IntEnum): + MANUAL = 0 # No updates by default, must call obx_sync_updates_request() manually + AUTO = 1 # Same as calling obx_sync_updates_request(sync, TRUE) + AUTO_NO_PUSHES = 2 # Same as calling obx_sync_updates_request(sync, FALSE) + + +class OBXSyncState(IntEnum): + CREATED = 1 + STARTED = 2 + CONNECTED = 3 + LOGGED_IN = 4 + DISCONNECTED = 5 + STOPPED = 6 + DEAD = 7 + + +class OBXSyncCode(IntEnum): + OK = 20 + REQ_REJECTED = 40 + CREDENTIALS_REJECTED = 43 + UNKNOWN = 50 + AUTH_UNREACHABLE = 53 + BAD_VERSION = 55 + CLIENT_ID_TAKEN = 61 + TX_VIOLATED_UNIQUE = 71 + + +class OBXSyncError(IntEnum): + REJECT_TX_NO_PERMISSION = 1 # Sync client received rejection of transaction writes due to missing permissions + + +class OBXSyncObjectType(IntEnum): + FlatBuffers = 1 + String = 2 + Raw = 3 + + +class OBX_sync_change(ctypes.Structure): + _fields_ = [ + ('entity_id', obx_schema_id), + ('puts', ctypes.POINTER(OBX_id_array)), + ('removals', ctypes.POINTER(OBX_id_array)), + ] + + +class OBX_sync_change_array(ctypes.Structure): + _fields_ = [ + ('list', ctypes.POINTER(OBX_sync_change)), + ('count', ctypes.c_size_t), + ] + + +class OBX_sync_object(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_int), # OBXSyncObjectType + ('id', ctypes.c_uint64), + ('data', ctypes.c_void_p), + ('size', ctypes.c_size_t), + ] + + +class OBX_sync_msg_objects(ctypes.Structure): + _fields_ = [ + ('topic', ctypes.c_void_p), + ('topic_size', ctypes.c_size_t), + ('objects', ctypes.POINTER(OBX_sync_object)), + ('count', ctypes.c_size_t), + ] + + +class OBX_sync_msg_objects_builder(ctypes.Structure): + pass + + +OBX_sync_msg_objects_builder_p = ctypes.POINTER(OBX_sync_msg_objects_builder) + +# Define callback types for sync listeners +OBX_sync_listener_connect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_disconnect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_login = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_login_failure = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncCode +OBX_sync_listener_complete = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_error = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncError +OBX_sync_listener_change = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_change_array)) +OBX_sync_listener_server_time = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64) +OBX_sync_listener_msg_objects = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_msg_objects)) + + # manually configure error methods, we can't use `fn()` defined below yet due to circular dependencies C.obx_last_error_message.restype = ctypes.c_char_p C.obx_last_error_code.restype = obx_err @@ -310,6 +427,11 @@ def check_obx_err(code: obx_err, func, args) -> obx_err: raise create_db_error(code) return code +def check_obx_success(code: obx_err) -> bool: + if code == DbErrorCode.OBX_NO_SUCCESS: + return False + check_obx_err(code, None, None) + return True def check_obx_qb_cond(qb_cond: obx_qb_cond, func, args) -> obx_qb_cond: """ Raises an exception if obx_qb_cond is not successful. """ @@ -1068,3 +1190,30 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): OBXBackupRestoreFlags_None = 0 OBXBackupRestoreFlags_OverwriteExistingData = 1 + +obx_sync = c_fn("obx_sync", obx_err, [OBX_store_p, ctypes.c_char_p]) +obx_sync_urls = c_fn("obx_sync_urls", obx_err, [OBX_store_p, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t]) + + +obx_sync_credentials = c_fn_rc('obx_sync_credentials', + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t]) +obx_sync_credentials_user_password = c_fn_rc('obx_sync_credentials_user_password', + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, + ctypes.c_char_p]) +obx_sync_credentials_add = c_fn_rc('obx_sync_credentials_add', + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_bool]) +obx_sync_credentials_add_user_password = c_fn_rc('obx_sync_credentials_add_user_password', + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, ctypes.c_char_p, + ctypes.c_bool]) + +obx_sync_request_updates_mode = c_fn_rc('obx_sync_request_updates_mode', [OBX_sync_p, OBXRequestUpdatesMode]) + +obx_sync_start = c_fn_rc('obx_sync_start', [OBX_sync_p]) +obx_sync_stop = c_fn_rc('obx_sync_stop', [OBX_sync_p]) + +obx_sync_trigger_reconnect = c_fn_rc('obx_sync_trigger_reconnect', [OBX_sync_p]) + +obx_sync_protocol_version = c_fn('obx_sync_protocol_version', ctypes.c_uint32, []) +obx_sync_protocol_version_server = c_fn('obx_sync_protocol_version_server', ctypes.c_uint32, [OBX_sync_p]) + +obx_sync_close = c_fn_rc('obx_sync_close', [OBX_sync_p]) \ No newline at end of file diff --git a/objectbox/store.py b/objectbox/store.py index c41e452..028b370 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -285,3 +285,6 @@ def remove_db_files(db_dir: str): Path to DB directory. """ c.obx_remove_db_files(c.c_str(db_dir)) + + def c_store(self): + return self._c_store diff --git a/objectbox/sync.py b/objectbox/sync.py new file mode 100644 index 0000000..91b73ca --- /dev/null +++ b/objectbox/sync.py @@ -0,0 +1,163 @@ +import ctypes +import c as c +from objectbox import Store +from objectbox.c import c_array_pointer + + +class SyncCredentials: + + def __init__(self, credential_type: c.OBXSyncCredentialsType): + self.type = credential_type + + @staticmethod + def none() -> 'SyncCredentials': + return SyncCredentialsNone() + + @staticmethod + def shared_secret_string(secret: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.SHARED_SECRET_SIPPED, secret.encode('utf-8')) + + @staticmethod + def google_auth(secret: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.GOOGLE_AUTH, secret.encode('utf-8')) + + @staticmethod + def user_and_password(username: str, password: str) -> 'SyncCredentials': + return SyncCredentialsUserPassword(c.OBXSyncCredentialsType.USER_PASSWORD, username, password) + + @staticmethod + def jwt_id_token(jwt_id_token: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_ID, jwt_id_token.encode('utf-8')) + + @staticmethod + def jwt_access_token(jwt_access_token: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_ACCESS, jwt_access_token.encode('utf-8')) + + @staticmethod + def jwt_refresh_token(jwt_refresh_token: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_REFRESH, jwt_refresh_token.encode('utf-8')) + + @staticmethod + def jwt_custom_token(jwt_custom_token: str) -> 'SyncCredentials': + return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_CUSTOM, jwt_custom_token.encode('utf-8')) + + +class SyncCredentialsNone(SyncCredentials): + def __init__(self): + super().__init__(c.OBXSyncCredentialsType.NONE) + + +class SyncCredentialsSecret(SyncCredentials): + def __init__(self, credential_type: c.OBXSyncCredentialsType, secret: bytes): + super().__init__(credential_type) + self.secret = secret + + +class SyncCredentialsUserPassword(SyncCredentials): + def __init__(self, credential_type: c.OBXSyncCredentialsType, username: str, password: str): + super().__init__(credential_type) + self.username = username + self.password = password + + +class SyncState: + UNKNOWN = 'unknown' + CREATED = 'created' + STARTED = 'started' + CONNECTED = 'connected' + LOGGED_IN = 'logged_in' + DISCONNECTED = 'disconnected' + STOPPED = 'stopped' + DEAD = 'dead' + + +class SyncRequestUpdatesMode: + MANUAL = 'manual' + AUTO = 'auto' + AUTO_NO_PUSHES = 'auto_no_pushes' + + +class SyncConnectionEvent: + CONNECTED = 'connected' + DISCONNECTED = 'disconnected' + + +class SyncLoginEvent: + LOGGED_IN = 'logged_in' + CREDENTIALS_REJECTED = 'credentials_rejected' + UNKNOWN_ERROR = 'unknown_error' + + +class SyncChange: + def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list[int]): + self.entity_id = entity_id + self.entity = entity + self.puts = puts + self.removals = removals + + +class SyncClient: + + def __init__(self, store: Store, server_urls: list[str], credentials: list[SyncCredentials], + filter_variables: dict[str, str] | None = None): + if not server_urls: + raise ValueError("Provide at least one server URL") + + if not Sync.is_available(): + raise RuntimeError( + 'Sync is not available in the loaded ObjectBox runtime library. ' + 'Please visit https://objectbox.io/sync/ for options.') + + self.__store = store + self.__server_urls = server_urls + self.__credentials = credentials + + self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c_array_pointer(server_urls, ctypes.c_char_p), + len(server_urls)) + + def set_credentials(self, credentials: SyncCredentials): + if isinstance(credentials, SyncCredentialsNone): + c.obx_sync_credentials(self.__c_sync_client_ptr, credentials.type, None, 0) + elif isinstance(credentials, SyncCredentialsUserPassword): + c.obx_sync_credentials_user_password(self.__c_sync_client_ptr, + credentials.type, + credentials.username.encode('utf-8'), + credentials.password.encode('utf-8')) + elif isinstance(credentials, SyncCredentialsSecret): + c.obx_sync_credentials(self.__c_sync_client_ptr, credentials.type, + credentials.secret, + len(credentials.secret)) + + def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): + if mode == SyncRequestUpdatesMode.MANUAL: + c_mode = c.OBXRequestUpdatesMode.MANUAL + elif mode == SyncRequestUpdatesMode.AUTO: + c_mode = c.OBXRequestUpdatesMode.AUTO + elif mode == SyncRequestUpdatesMode.AUTO_NO_PUSHES: + c_mode = c.OBXRequestUpdatesMode.AUTO_NO_PUSHES + else: + raise ValueError(f"Invalid mode: {mode}") + c.obx_sync_request_updates_mode(self.__c_sync_client_ptr, c_mode) + + def start(self): + c.obx_sync_start(self.__c_sync_client_ptr) + + def stop(self): + c.obx_sync_stop(self.__c_sync_client_ptr) + + def trigger_reconnect(self) -> bool: + return c.check_obx_success(c.obx_sync_trigger_reconnect(self.__c_sync_client_ptr)) + + @staticmethod + def protocol_version() -> int: + return c.obx_sync_protocol_version() + + def protocol_server_version(self) -> int: + return c.obx_sync_protocol_version_server(self.__c_sync_client_ptr) + + def close(self): + c.obx_sync_close(self.__c_sync_client_ptr) + self.__c_sync_client_ptr = None + + def is_closed(self) -> bool: + return self.__c_sync_client_ptr is None From bf41581346277c9f8d796fb1e279ba1d6f2dea0f Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 23 Dec 2025 21:25:02 +0530 Subject: [PATCH 02/21] Fix type error, add obx_sync_state call in SyncClient --- objectbox/c.py | 24 ++++++++------ objectbox/sync.py | 82 +++++++++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 6addd61..4520ddb 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -27,7 +27,7 @@ # Version of the library used by the binding. This version is checked at runtime to ensure binary compatibility. # Don't forget to update download-c-lib.py when upgrading to a newer version. -required_version = "4.0.0" +required_version = "5.0.0" def shlib_name(library: str) -> str: @@ -266,8 +266,12 @@ class OBX_sync_server(ctypes.Structure): OBX_sync_server_p = ctypes.POINTER(OBX_sync_server) +OBXSyncCredentialsType = ctypes.c_int +OBXRequestUpdatesMode = ctypes.c_int +OBXSyncState = ctypes.c_int +OBXSyncCode = ctypes.c_int -class OBXSyncCredentialsType(IntEnum): +class SyncCredentialsType(IntEnum): NONE = 1 SHARED_SECRET = 2 # Deprecated, use SHARED_SECRET_SIPPED instead GOOGLE_AUTH = 3 @@ -280,13 +284,13 @@ class OBXSyncCredentialsType(IntEnum): JWT_CUSTOM = 10 # JSON Web Token (JWT): custom token type -class OBXRequestUpdatesMode(IntEnum): +class RequestUpdatesMode(IntEnum): MANUAL = 0 # No updates by default, must call obx_sync_updates_request() manually AUTO = 1 # Same as calling obx_sync_updates_request(sync, TRUE) AUTO_NO_PUSHES = 2 # Same as calling obx_sync_updates_request(sync, FALSE) -class OBXSyncState(IntEnum): +class SyncState(IntEnum): CREATED = 1 STARTED = 2 CONNECTED = 3 @@ -296,7 +300,7 @@ class OBXSyncState(IntEnum): DEAD = 7 -class OBXSyncCode(IntEnum): +class SyncCode(IntEnum): OK = 20 REQ_REJECTED = 40 CREDENTIALS_REJECTED = 43 @@ -1191,21 +1195,23 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): OBXBackupRestoreFlags_None = 0 OBXBackupRestoreFlags_OverwriteExistingData = 1 -obx_sync = c_fn("obx_sync", obx_err, [OBX_store_p, ctypes.c_char_p]) -obx_sync_urls = c_fn("obx_sync_urls", obx_err, [OBX_store_p, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t]) +obx_sync = c_fn("obx_sync", OBX_sync_p, [OBX_store_p, ctypes.c_char_p]) +obx_sync_urls = c_fn("obx_sync_urls", OBX_sync_p, [OBX_store_p, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t]) obx_sync_credentials = c_fn_rc('obx_sync_credentials', - [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t]) + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t]) obx_sync_credentials_user_password = c_fn_rc('obx_sync_credentials_user_password', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, ctypes.c_char_p]) obx_sync_credentials_add = c_fn_rc('obx_sync_credentials_add', - [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_bool]) + [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_bool]) obx_sync_credentials_add_user_password = c_fn_rc('obx_sync_credentials_add_user_password', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]) +obx_sync_state = c_fn('obx_sync_state', OBXSyncState, [OBX_sync_p]) + obx_sync_request_updates_mode = c_fn_rc('obx_sync_request_updates_mode', [OBX_sync_p, OBXRequestUpdatesMode]) obx_sync_start = c_fn_rc('obx_sync_start', [OBX_sync_p]) diff --git a/objectbox/sync.py b/objectbox/sync.py index 91b73ca..2f14e42 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -1,12 +1,11 @@ import ctypes -import c as c +import objectbox.c as c from objectbox import Store -from objectbox.c import c_array_pointer - +from enum import Enum, auto class SyncCredentials: - def __init__(self, credential_type: c.OBXSyncCredentialsType): + def __init__(self, credential_type: c.SyncCredentialsType): self.type = credential_type @staticmethod @@ -15,60 +14,60 @@ def none() -> 'SyncCredentials': @staticmethod def shared_secret_string(secret: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.SHARED_SECRET_SIPPED, secret.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.SHARED_SECRET_SIPPED, secret.encode('utf-8')) @staticmethod def google_auth(secret: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.GOOGLE_AUTH, secret.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.GOOGLE_AUTH, secret.encode('utf-8')) @staticmethod def user_and_password(username: str, password: str) -> 'SyncCredentials': - return SyncCredentialsUserPassword(c.OBXSyncCredentialsType.USER_PASSWORD, username, password) + return SyncCredentialsUserPassword(c.SyncCredentialsType.USER_PASSWORD, username, password) @staticmethod def jwt_id_token(jwt_id_token: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_ID, jwt_id_token.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ID, jwt_id_token.encode('utf-8')) @staticmethod def jwt_access_token(jwt_access_token: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_ACCESS, jwt_access_token.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ACCESS, jwt_access_token.encode('utf-8')) @staticmethod def jwt_refresh_token(jwt_refresh_token: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_REFRESH, jwt_refresh_token.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.JWT_REFRESH, jwt_refresh_token.encode('utf-8')) @staticmethod def jwt_custom_token(jwt_custom_token: str) -> 'SyncCredentials': - return SyncCredentialsSecret(c.OBXSyncCredentialsType.JWT_CUSTOM, jwt_custom_token.encode('utf-8')) + return SyncCredentialsSecret(c.SyncCredentialsType.JWT_CUSTOM, jwt_custom_token.encode('utf-8')) class SyncCredentialsNone(SyncCredentials): def __init__(self): - super().__init__(c.OBXSyncCredentialsType.NONE) + super().__init__(c.SyncCredentialsType.NONE) class SyncCredentialsSecret(SyncCredentials): - def __init__(self, credential_type: c.OBXSyncCredentialsType, secret: bytes): + def __init__(self, credential_type: c.SyncCredentialsType, secret: bytes): super().__init__(credential_type) self.secret = secret class SyncCredentialsUserPassword(SyncCredentials): - def __init__(self, credential_type: c.OBXSyncCredentialsType, username: str, password: str): + def __init__(self, credential_type: c.SyncCredentialsType, username: str, password: str): super().__init__(credential_type) self.username = username self.password = password -class SyncState: - UNKNOWN = 'unknown' - CREATED = 'created' - STARTED = 'started' - CONNECTED = 'connected' - LOGGED_IN = 'logged_in' - DISCONNECTED = 'disconnected' - STOPPED = 'stopped' - DEAD = 'dead' +class SyncState(Enum): + UNKNOWN = auto() + CREATED = auto() + STARTED = auto() + CONNECTED = auto() + LOGGED_IN = auto() + DISCONNECTED = auto() + STOPPED = auto() + DEAD = auto() class SyncRequestUpdatesMode: @@ -103,16 +102,18 @@ def __init__(self, store: Store, server_urls: list[str], credentials: list[SyncC if not server_urls: raise ValueError("Provide at least one server URL") - if not Sync.is_available(): - raise RuntimeError( - 'Sync is not available in the loaded ObjectBox runtime library. ' - 'Please visit https://objectbox.io/sync/ for options.') + # TODO: Implement sync availability check + # if not c.Sync.is_available(): + # raise RuntimeError( + # 'Sync is not available in the loaded ObjectBox runtime library. ' + # 'Please visit https://objectbox.io/sync/ for options.') self.__store = store self.__server_urls = server_urls self.__credentials = credentials - self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c_array_pointer(server_urls, ctypes.c_char_p), + server_urls = [url.encode('utf-8') for url in server_urls] + self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c.c_array_pointer(server_urls, ctypes.c_char_p), len(server_urls)) def set_credentials(self, credentials: SyncCredentials): @@ -130,15 +131,34 @@ def set_credentials(self, credentials: SyncCredentials): def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): if mode == SyncRequestUpdatesMode.MANUAL: - c_mode = c.OBXRequestUpdatesMode.MANUAL + c_mode = c.RequestUpdatesMode.MANUAL elif mode == SyncRequestUpdatesMode.AUTO: - c_mode = c.OBXRequestUpdatesMode.AUTO + c_mode = c.RequestUpdatesMode.AUTO elif mode == SyncRequestUpdatesMode.AUTO_NO_PUSHES: - c_mode = c.OBXRequestUpdatesMode.AUTO_NO_PUSHES + c_mode = c.RequestUpdatesMode.AUTO_NO_PUSHES else: raise ValueError(f"Invalid mode: {mode}") c.obx_sync_request_updates_mode(self.__c_sync_client_ptr, c_mode) + def get_sync_state(self) -> SyncState: + c_state = c.obx_sync_state(self.__c_sync_client_ptr) + if c_state == c.SyncState.CREATED: + return SyncState.CREATED + elif c_state == c.SyncState.STARTED: + return SyncState.STARTED + elif c_state == c.SyncState.CONNECTED: + return SyncState.CONNECTED + elif c_state == c.SyncState.LOGGED_IN: + return SyncState.LOGGED_IN + elif c_state == c.SyncState.DISCONNECTED: + return SyncState.DISCONNECTED + elif c_state == c.SyncState.STOPPED: + return SyncState.STOPPED + elif c_state == c.SyncState.DEAD: + return SyncState.DEAD + else: + return SyncState.UNKNOWN + def start(self): c.obx_sync_start(self.__c_sync_client_ptr) From dabc331fde03d16da9ec016a01ce39a5bff3c4dc Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 23 Dec 2025 21:25:34 +0530 Subject: [PATCH 03/21] Upgrade to OBX 5.0.0, download Sync version of C library only (temporarily) --- download-c-lib.py | 6 +++--- tests/test_sync.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/test_sync.py diff --git a/download-c-lib.py b/download-c-lib.py index 58d711d..9e1dedf 100644 --- a/download-c-lib.py +++ b/download-c-lib.py @@ -6,8 +6,8 @@ # Script used to download objectbox-c shared libraries for all supported platforms. Execute by running `make get-lib` # on first checkout of this repo and any time after changing the objectbox-c lib version. -version = "v4.0.0" # see objectbox/c.py required_version -variant = 'objectbox' # or 'objectbox-sync' +version = "v5.0.0" # see objectbox/c.py required_version +variant = 'objectbox-sync' # or 'objectbox-sync' base_url = "https://github.com/objectbox/objectbox-c/releases/download/" @@ -21,7 +21,7 @@ "x86_64/libobjectbox.so": "linux-x64.tar.gz", "aarch64/libobjectbox.so": "linux-aarch64.tar.gz", "armv7l/libobjectbox.so": "linux-armv7hf.tar.gz", - "armv6l/libobjectbox.so": "linux-armv6hf.tar.gz", + #"armv6l/libobjectbox.so": "linux-armv6hf.tar.gz", # mac "macos-universal/libobjectbox.dylib": "macos-universal.zip", diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..5a83c76 --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,18 @@ +from objectbox.sync import * + + +def test_sync_protocol_version(): + version = SyncClient.protocol_version() + assert version >= 1 + + +def test_sync_client_states(test_store): + server_urls = ["ws://localhost:9999"] + credentials = [SyncCredentials.none()] + client = SyncClient(test_store, server_urls, credentials) + assert client.get_sync_state() == SyncState.CREATED + client.start() + assert client.get_sync_state() == SyncState.STARTED + client.stop() + assert client.get_sync_state() == SyncState.STOPPED + client.close() From 50d3cb089ac2f735484502b00e6fe87c9137a361 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 24 Dec 2025 13:13:57 +0530 Subject: [PATCH 04/21] Add login, connection and error listeners --- objectbox/c.py | 22 +++++------ objectbox/sync.py | 92 ++++++++++++++++++++++++++++++++++++++++++---- tests/conftest.py | 52 ++++++++++++++++++++++++++ tests/test_sync.py | 20 +++++++++- 4 files changed, 164 insertions(+), 22 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 4520ddb..1481b6c 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -299,18 +299,6 @@ class SyncState(IntEnum): STOPPED = 6 DEAD = 7 - -class SyncCode(IntEnum): - OK = 20 - REQ_REJECTED = 40 - CREDENTIALS_REJECTED = 43 - UNKNOWN = 50 - AUTH_UNREACHABLE = 53 - BAD_VERSION = 55 - CLIENT_ID_TAKEN = 61 - TX_VIOLATED_UNIQUE = 71 - - class OBXSyncError(IntEnum): REJECT_TX_NO_PERMISSION = 1 # Sync client received rejection of transaction writes due to missing permissions @@ -1222,4 +1210,12 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_sync_protocol_version = c_fn('obx_sync_protocol_version', ctypes.c_uint32, []) obx_sync_protocol_version_server = c_fn('obx_sync_protocol_version_server', ctypes.c_uint32, [OBX_sync_p]) -obx_sync_close = c_fn_rc('obx_sync_close', [OBX_sync_p]) \ No newline at end of file +obx_sync_close = c_fn_rc('obx_sync_close', [OBX_sync_p]) + +obx_sync_listener_connect = c_fn('obx_sync_listener_connect', None, [OBX_sync_p, OBX_sync_listener_connect, ctypes.c_void_p]) +obx_sync_listener_disconnect = c_fn('obx_sync_listener_disconnect', None, [OBX_sync_p, OBX_sync_listener_disconnect, ctypes.c_void_p]) +obx_sync_listener_login = c_fn('obx_sync_listener_login', None, [OBX_sync_p, OBX_sync_listener_login, ctypes.c_void_p]) +obx_sync_listener_login_failure = c_fn('obx_sync_listener_login_failure', None, [OBX_sync_p, OBX_sync_listener_login_failure, ctypes.c_void_p]) +obx_sync_listener_error = c_fn('obx_sync_listener_error', None, [OBX_sync_p, OBX_sync_listener_error, ctypes.c_void_p]) + +obx_sync_wait_for_logged_in_state = c_fn_rc('obx_sync_wait_for_logged_in_state', [OBX_sync_p, ctypes.c_uint64]) \ No newline at end of file diff --git a/objectbox/sync.py b/objectbox/sync.py index 2f14e42..17072e9 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -1,7 +1,13 @@ import ctypes +from collections.abc import Callable +from ctypes import c_void_p + import objectbox.c as c from objectbox import Store -from enum import Enum, auto +from enum import Enum, auto, IntEnum + +from objectbox.c import OBX_sync_listener_login + class SyncCredentials: @@ -86,6 +92,15 @@ class SyncLoginEvent: CREDENTIALS_REJECTED = 'credentials_rejected' UNKNOWN_ERROR = 'unknown_error' +class SyncCode(IntEnum): + OK = 20 + REQ_REJECTED = 40 + CREDENTIALS_REJECTED = 43 + UNKNOWN = 50 + AUTH_UNREACHABLE = 53 + BAD_VERSION = 55 + CLIENT_ID_TAKEN = 61 + TX_VIOLATED_UNIQUE = 71 class SyncChange: def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list[int]): @@ -94,11 +109,36 @@ def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list self.puts = puts self.removals = removals +class SyncLoginListener: + + def on_logged_in(self): + pass + + def on_login_failed(self, sync_login_code: SyncCode): + pass + +class SyncConnectionListener: + + def on_connected(self): + pass + + def on_disconnected(self): + pass + +class SyncErrorListener: + + def on_error(self, sync_error_code: int): + pass class SyncClient: - def __init__(self, store: Store, server_urls: list[str], credentials: list[SyncCredentials], + def __init__(self, store: Store, server_urls: list[str], filter_variables: dict[str, str] | None = None): + self.__c_login_listener = None + self.__c_login_failure_listener = None + self.__c_connect_listener = None + self.__c_disconnect_listener = None + self.__c_error_listener = None if not server_urls: raise ValueError("Provide at least one server URL") @@ -109,14 +149,13 @@ def __init__(self, store: Store, server_urls: list[str], credentials: list[SyncC # 'Please visit https://objectbox.io/sync/ for options.') self.__store = store - self.__server_urls = server_urls - self.__credentials = credentials + self.__server_urls = [url.encode('utf-8') for url in server_urls] - server_urls = [url.encode('utf-8') for url in server_urls] - self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c.c_array_pointer(server_urls, ctypes.c_char_p), - len(server_urls)) + self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c.c_array_pointer(self.__server_urls, ctypes.c_char_p), + len(self.__server_urls)) def set_credentials(self, credentials: SyncCredentials): + self.__credentials = credentials if isinstance(credentials, SyncCredentialsNone): c.obx_sync_credentials(self.__c_sync_client_ptr, credentials.type, None, 0) elif isinstance(credentials, SyncCredentialsUserPassword): @@ -181,3 +220,42 @@ def close(self): def is_closed(self) -> bool: return self.__c_sync_client_ptr is None + + def set_login_listener(self, login_listener: SyncLoginListener): + self.__c_login_listener = c.OBX_sync_listener_login(lambda arg: login_listener.on_logged_in()) + self.__c_login_failure_listener = c.OBX_sync_listener_login_failure(lambda arg, sync_login_code: login_listener.on_login_failed(sync_login_code)) + c.obx_sync_listener_login( + self.__c_sync_client_ptr, + self.__c_login_listener, + None + ) + c.obx_sync_listener_login_failure( + self.__c_sync_client_ptr, + self.__c_login_failure_listener, + None + ) + + def set_connection_listener(self, connection_listener: SyncConnectionListener): + self.__c_connect_listener = c.OBX_sync_listener_connect(lambda arg: connection_listener.on_connected()) + self.__c_disconnect_listener = c.OBX_sync_listener_disconnect(lambda arg: connection_listener.on_disconnected()) + c.obx_sync_listener_connect( + self.__c_sync_client_ptr, + self.__c_connect_listener, + None + ) + c.obx_sync_listener_disconnect( + self.__c_sync_client_ptr, + self.__c_disconnect_listener, + None + ) + + def set_error_listener(self, error_listener: SyncErrorListener): + self.__c_error_listener = c.OBX_sync_listener_error(lambda arg, sync_error_code: error_listener.on_error(sync_error_code)) + c.obx_sync_listener_error( + self.__c_sync_client_ptr, + self.__c_error_listener, + None + ) + + def wait_for_logged_in_state(self, timeout_millis: int): + c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 7c4bca4..8ccaf22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import pytest from objectbox.logger import logger +from objectbox.sync import SyncLoginListener, SyncConnectionListener, SyncErrorListener from common import * @@ -19,3 +20,54 @@ def test_store(): store = create_test_store() yield store store.close() + +class TestLoginListener(SyncLoginListener): + def __init__(self): + self.logged_in_called = False + self.login_failure_code = None + + def on_logged_in(self): + self.logged_in_called = True + + def on_login_failed(self, sync_login_code: int): + self.login_failure_code = sync_login_code + + +class TestConnectionListener(SyncConnectionListener): + def __init__(self): + self.connected_called = False + self.disconnected_called = False + + def on_connected(self): + self.connected_called = True + + def on_disconnected(self): + self.disconnected_called = True + + +class TestErrorListener(SyncErrorListener): + def __init__(self): + self.sync_error_code = None + + def on_error(self, sync_error_code: int): + self.sync_error_code = sync_error_code + +@pytest.fixture +def connection_listener(): + listener = TestConnectionListener() + yield listener + listener.connected_called = False + listener.disconnected_called = False + +@pytest.fixture +def login_listener(): + listener = TestLoginListener() + yield listener + listener.logged_in_called = False + listener.login_failure_code = None + +@pytest.fixture +def error_listener(): + listener = TestErrorListener() + yield listener + listener.sync_error_code = None \ No newline at end of file diff --git a/tests/test_sync.py b/tests/test_sync.py index 5a83c76..77a2a43 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,11 +1,10 @@ +from time import sleep from objectbox.sync import * - def test_sync_protocol_version(): version = SyncClient.protocol_version() assert version >= 1 - def test_sync_client_states(test_store): server_urls = ["ws://localhost:9999"] credentials = [SyncCredentials.none()] @@ -16,3 +15,20 @@ def test_sync_client_states(test_store): client.stop() assert client.get_sync_state() == SyncState.STOPPED client.close() + +def test_sync_listener(test_store, login_listener, connection_listener): + server_urls = ["ws://127.0.0.1:9999"] + client = SyncClient(test_store, server_urls) + client.set_credentials(SyncCredentials.shared_secret_string("shared_secret")) + client.set_login_listener(login_listener) + client.set_connection_listener(connection_listener) + + client.start() + sleep(1) + client.stop() + client.close() + + assert login_listener.login_failure_code is not None + assert login_listener.login_failure_code == SyncCode.CREDENTIALS_REJECTED + assert connection_listener.connected_called + assert connection_listener.disconnected_called From 7b86cfc42af0227e17310cb85e2cf90f6402e816 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 08:14:34 +0530 Subject: [PATCH 05/21] Add methods for filter variables --- objectbox/c.py | 15 ++++++++++++--- objectbox/sync.py | 32 +++++++++++++++++++++++--------- tests/test_sync.py | 23 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 1481b6c..8cd93a8 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -16,10 +16,12 @@ import ctypes.util import os import platform -from objectbox.version import Version +from ctypes import c_char_p from typing import * + import numpy as np -from enum import IntEnum + +from objectbox.version import Version # This file contains C-API bindings based on lib/objectbox.h, linking to the 'objectbox' shared library. # The bindings are implementing using ctypes, see https://docs.python.org/dev/library/ctypes.html for introduction. @@ -1218,4 +1220,11 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_sync_listener_login_failure = c_fn('obx_sync_listener_login_failure', None, [OBX_sync_p, OBX_sync_listener_login_failure, ctypes.c_void_p]) obx_sync_listener_error = c_fn('obx_sync_listener_error', None, [OBX_sync_p, OBX_sync_listener_error, ctypes.c_void_p]) -obx_sync_wait_for_logged_in_state = c_fn_rc('obx_sync_wait_for_logged_in_state', [OBX_sync_p, ctypes.c_uint64]) \ No newline at end of file +obx_sync_wait_for_logged_in_state = c_fn_rc('obx_sync_wait_for_logged_in_state', [OBX_sync_p, ctypes.c_uint64]) + +obx_sync_filter_variables_put = c_fn_rc('obx_sync_filter_variables_put', + [OBX_sync_p, c_char_p, c_char_p]) +obx_sync_filter_variables_remove = c_fn_rc('obx_sync_filter_variables_remove', + [OBX_sync_p, c_char_p]) +obx_sync_filter_variables_remove_all = c_fn_rc('obx_sync_filter_variables_remove_all', + [OBX_sync_p]) diff --git a/objectbox/sync.py b/objectbox/sync.py index 17072e9..e2ce8b1 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -1,12 +1,8 @@ import ctypes -from collections.abc import Callable -from ctypes import c_void_p +from enum import Enum, auto, IntEnum import objectbox.c as c from objectbox import Store -from enum import Enum, auto, IntEnum - -from objectbox.c import OBX_sync_listener_login class SyncCredentials: @@ -92,6 +88,7 @@ class SyncLoginEvent: CREDENTIALS_REJECTED = 'credentials_rejected' UNKNOWN_ERROR = 'unknown_error' + class SyncCode(IntEnum): OK = 20 REQ_REJECTED = 40 @@ -102,6 +99,7 @@ class SyncCode(IntEnum): CLIENT_ID_TAKEN = 61 TX_VIOLATED_UNIQUE = 71 + class SyncChange: def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list[int]): self.entity_id = entity_id @@ -109,6 +107,7 @@ def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list self.puts = puts self.removals = removals + class SyncLoginListener: def on_logged_in(self): @@ -117,6 +116,7 @@ def on_logged_in(self): def on_login_failed(self, sync_login_code: SyncCode): pass + class SyncConnectionListener: def on_connected(self): @@ -125,11 +125,13 @@ def on_connected(self): def on_disconnected(self): pass + class SyncErrorListener: def on_error(self, sync_error_code: int): pass + class SyncClient: def __init__(self, store: Store, server_urls: list[str], @@ -151,7 +153,8 @@ def __init__(self, store: Store, server_urls: list[str], self.__store = store self.__server_urls = [url.encode('utf-8') for url in server_urls] - self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), c.c_array_pointer(self.__server_urls, ctypes.c_char_p), + self.__c_sync_client_ptr = c.obx_sync_urls(store.c_store(), + c.c_array_pointer(self.__server_urls, ctypes.c_char_p), len(self.__server_urls)) def set_credentials(self, credentials: SyncCredentials): @@ -223,7 +226,8 @@ def is_closed(self) -> bool: def set_login_listener(self, login_listener: SyncLoginListener): self.__c_login_listener = c.OBX_sync_listener_login(lambda arg: login_listener.on_logged_in()) - self.__c_login_failure_listener = c.OBX_sync_listener_login_failure(lambda arg, sync_login_code: login_listener.on_login_failed(sync_login_code)) + self.__c_login_failure_listener = c.OBX_sync_listener_login_failure( + lambda arg, sync_login_code: login_listener.on_login_failed(sync_login_code)) c.obx_sync_listener_login( self.__c_sync_client_ptr, self.__c_login_listener, @@ -250,7 +254,8 @@ def set_connection_listener(self, connection_listener: SyncConnectionListener): ) def set_error_listener(self, error_listener: SyncErrorListener): - self.__c_error_listener = c.OBX_sync_listener_error(lambda arg, sync_error_code: error_listener.on_error(sync_error_code)) + self.__c_error_listener = c.OBX_sync_listener_error( + lambda arg, sync_error_code: error_listener.on_error(sync_error_code)) c.obx_sync_listener_error( self.__c_sync_client_ptr, self.__c_error_listener, @@ -258,4 +263,13 @@ def set_error_listener(self, error_listener: SyncErrorListener): ) def wait_for_logged_in_state(self, timeout_millis: int): - c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis) \ No newline at end of file + c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis) + + def add_filter_variable(self, name: str, value: str): + c.obx_sync_filter_variables_put(self.__c_sync_client_ptr, name.encode('utf-8'), value.encode('utf-8')) + + def remove_filter_variable(self, name: str): + c.obx_sync_filter_variables_remove(self.__c_sync_client_ptr, name.encode('utf-8')) + + def remove_all_filter_variables(self): + c.obx_sync_filter_variables_remove_all(self.__c_sync_client_ptr) diff --git a/tests/test_sync.py b/tests/test_sync.py index 77a2a43..c47ae0c 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,6 +1,11 @@ from time import sleep + +import pytest + +from objectbox.exceptions import IllegalArgumentError from objectbox.sync import * + def test_sync_protocol_version(): version = SyncClient.protocol_version() assert version >= 1 @@ -32,3 +37,21 @@ def test_sync_listener(test_store, login_listener, connection_listener): assert login_listener.login_failure_code == SyncCode.CREDENTIALS_REJECTED assert connection_listener.connected_called assert connection_listener.disconnected_called + + +def test_filter_variables(test_store): + server_urls = ["ws://localhost:9999"] + + filter_vars = { + "name1": "val1", + "name2": "val2" + } + client = SyncClient(test_store, server_urls, filter_vars) + + client.add_filter_variable("name3", "val3") + client.remove_filter_variable("name1") + client.add_filter_variable("name4", "val4") + client.remove_all_filter_variables() + + with pytest.raises(IllegalArgumentError, match="Filter variables must have a name"): + client.add_filter_variable("", "val5") From 9a0d951c15ccea3d4d1dfdaa9102eda742c2cb4e Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 08:44:52 +0530 Subject: [PATCH 06/21] Add method for outgoing message count --- objectbox/c.py | 4 ++++ objectbox/sync.py | 5 +++++ tests/test_sync.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/objectbox/c.py b/objectbox/c.py index 8cd93a8..b074d9f 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -1228,3 +1228,7 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): [OBX_sync_p, c_char_p]) obx_sync_filter_variables_remove_all = c_fn_rc('obx_sync_filter_variables_remove_all', [OBX_sync_p]) + +# OBX_C_API obx_err obx_sync_outgoing_message_count(OBX_sync* sync, uint64_t limit, uint64_t* out_count); +obx_sync_outgoing_message_count = c_fn_rc('obx_sync_outgoing_message_count', + [OBX_sync_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)]) diff --git a/objectbox/sync.py b/objectbox/sync.py index e2ce8b1..233d07e 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -273,3 +273,8 @@ def remove_filter_variable(self, name: str): def remove_all_filter_variables(self): c.obx_sync_filter_variables_remove_all(self.__c_sync_client_ptr) + + def get_outgoing_message_count(self, limit: int = 0) -> int: + outgoing_message_count = ctypes.c_uint64(0) + c.obx_sync_outgoing_message_count(self.__c_sync_client_ptr, limit, ctypes.byref(outgoing_message_count)) + return outgoing_message_count.value diff --git a/tests/test_sync.py b/tests/test_sync.py index c47ae0c..d4e488a 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -55,3 +55,21 @@ def test_filter_variables(test_store): with pytest.raises(IllegalArgumentError, match="Filter variables must have a name"): client.add_filter_variable("", "val5") + + client.close() + + +def test_outgoing_message_count(test_store): + server_urls = ["ws://localhost:9999"] + client = SyncClient(test_store, server_urls) + + count = client.get_outgoing_message_count() + assert count == 0 + + count_limited = client.get_outgoing_message_count(limit=10) + assert count_limited == 0 + + client.close() + + with pytest.raises(IllegalArgumentError, match='Argument "sync" must not be null'): + client.get_outgoing_message_count() From 7bf29d4bc1f322a919a21aa2b78b577f80031797 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 10:05:48 +0530 Subject: [PATCH 07/21] Add method for setting multiple credentials --- objectbox/sync.py | 27 ++++++++++++++++++++++++++- tests/test_sync.py | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/objectbox/sync.py b/objectbox/sync.py index 233d07e..080e267 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -4,7 +4,6 @@ import objectbox.c as c from objectbox import Store - class SyncCredentials: def __init__(self, credential_type: c.SyncCredentialsType): @@ -171,6 +170,32 @@ def set_credentials(self, credentials: SyncCredentials): credentials.secret, len(credentials.secret)) + def set_multiple_credentials(self, credentials_list: list[SyncCredentials]): + if len(credentials_list) == 0: + raise ValueError("Provide at least one credential") + + for i in range(len(credentials_list)): + is_last = (i == len(credentials_list) - 1) + credentials = credentials_list[i] + + if isinstance(credentials, SyncCredentialsNone): + raise ValueError("SyncCredentials.none() is not supported, use set_credentials() instead") + + if isinstance(credentials, SyncCredentialsUserPassword): + c.obx_sync_credentials_add_user_password(self.__c_sync_client_ptr, + credentials.type, + credentials.username.encode('utf-8'), + credentials.password.encode('utf-8'), + is_last + ) + elif isinstance(credentials, SyncCredentialsSecret): + c.obx_sync_credentials_add(self.__c_sync_client_ptr, + credentials.type, + credentials.secret, + len(credentials.secret), + is_last) + + def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): if mode == SyncRequestUpdatesMode.MANUAL: c_mode = c.RequestUpdatesMode.MANUAL diff --git a/tests/test_sync.py b/tests/test_sync.py index d4e488a..6f223cb 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -73,3 +73,26 @@ def test_outgoing_message_count(test_store): with pytest.raises(IllegalArgumentError, match='Argument "sync" must not be null'): client.get_outgoing_message_count() + + +def test_multiple_credentials(test_store): + server_urls = ["ws://localhost:9999"] + client = SyncClient(test_store, server_urls) + + # empty list should raise ValueError + with pytest.raises(ValueError, match='Provide at least one credential'): + client.set_multiple_credentials([]) + + # SyncCredentials.none() is not supported with multiple credentials + with pytest.raises(ValueError, match=r'SyncCredentials.none\(\) is not supported, use set_credentials\(\) instead'): + client.set_multiple_credentials([SyncCredentials.none()]) + + client.set_multiple_credentials([ + SyncCredentials.google_auth("token_google"), + SyncCredentials.user_and_password("user1", "password"), + SyncCredentials.shared_secret_string("secret1"), + SyncCredentials.jwt_id_token("token1"), + SyncCredentials.jwt_access_token("token2"), + SyncCredentials.jwt_refresh_token("token3"), + SyncCredentials.jwt_custom_token("token4") + ]) From 7aacc1aa3a06532440a6798bcd73a98bf235769b Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 10:11:34 +0530 Subject: [PATCH 08/21] Remove credentials for SyncClient constructor in test_sync_client_states --- tests/test_sync.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index 6f223cb..b01a617 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -12,8 +12,7 @@ def test_sync_protocol_version(): def test_sync_client_states(test_store): server_urls = ["ws://localhost:9999"] - credentials = [SyncCredentials.none()] - client = SyncClient(test_store, server_urls, credentials) + client = SyncClient(test_store, server_urls) assert client.get_sync_state() == SyncState.CREATED client.start() assert client.get_sync_state() == SyncState.STARTED From 5a3bf3e0e950d7a5617e5c9a455dc5a3b949b1bd Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 10:52:51 +0530 Subject: [PATCH 09/21] Notify Sync client when underlying Store is closed When Store is closed, the Sync client should be closed too --- objectbox/store.py | 7 +++++++ objectbox/sync.py | 8 ++++++++ tests/test_sync.py | 9 +++++++++ 3 files changed, 24 insertions(+) diff --git a/objectbox/store.py b/objectbox/store.py index 028b370..3470bc8 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -127,6 +127,7 @@ def __init__(self, """ self._c_store = None + self._close_listeners: list[Callable[[], None]] = [] if not c_store: options = StoreOptions() try: @@ -272,6 +273,9 @@ def write_tx(self): def close(self): """Close database.""" + for listener in self._close_listeners.values(): + listener() + self._close_listeners.clear() c_store_to_close = self._c_store if c_store_to_close: self._c_store = None @@ -288,3 +292,6 @@ def remove_db_files(db_dir: str): def c_store(self): return self._c_store + + def add_store_close_listener(self, on_store_close: Callable[[], None]): + self._close_listeners.append(on_store_close) diff --git a/objectbox/sync.py b/objectbox/sync.py index 080e267..2f5cc1b 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -156,6 +156,14 @@ def __init__(self, store: Store, server_urls: list[str], c.c_array_pointer(self.__server_urls, ctypes.c_char_p), len(self.__server_urls)) + self.__store.add_store_close_listener(on_store_close=self.__close_sync_client_func()) + + def __close_sync_client_func(self): + def close_sync_client(): + self.close() + + return close_sync_client + def set_credentials(self, credentials: SyncCredentials): self.__credentials = credentials if isinstance(credentials, SyncCredentialsNone): diff --git a/tests/test_sync.py b/tests/test_sync.py index b01a617..9534df7 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -95,3 +95,12 @@ def test_multiple_credentials(test_store): SyncCredentials.jwt_refresh_token("token3"), SyncCredentials.jwt_custom_token("token4") ]) + + +def test_client_closed_when_store_closed(test_store): + server_urls = ["ws://localhost:9999"] + client = SyncClient(test_store, server_urls) + + assert not client.is_closed() + test_store.close() + assert client.is_closed() From 978a9a9a0c45fb7f7d908ef9654d270e2df65006 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 11:25:00 +0530 Subject: [PATCH 10/21] Add Sync class with static factory methods to construct SyncClient --- objectbox/c.py | 27 +++++++++++++++++++++++ objectbox/sync.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/objectbox/c.py b/objectbox/c.py index b074d9f..aaf84df 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -1232,3 +1232,30 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): # OBX_C_API obx_err obx_sync_outgoing_message_count(OBX_sync* sync, uint64_t limit, uint64_t* out_count); obx_sync_outgoing_message_count = c_fn_rc('obx_sync_outgoing_message_count', [OBX_sync_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)]) + +OBXFeature = ctypes.c_int + + +class Feature(IntEnum): + ResultArray = 1 + TimeSeries = 2 + Sync = 3 + DebugLog = 4 + Admin = 5 + Tree = 6 + SyncServer = 7 + WebSockets = 8 + Cluster = 9 + HttpServer = 10 + GraphQL = 11 + Backup = 12 + Lmdb = 13 + VectorSearch = 14 + Wal = 15 + SyncMongoDb = 16 + Auth = 17 + Trial = 18 + SyncFilters = 19 + + +obx_has_feature = c_fn('obx_has_feature', ctypes.c_bool, [OBXFeature]) diff --git a/objectbox/sync.py b/objectbox/sync.py index 2f5cc1b..16050b9 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -311,3 +311,58 @@ def get_outgoing_message_count(self, limit: int = 0) -> int: outgoing_message_count = ctypes.c_uint64(0) c.obx_sync_outgoing_message_count(self.__c_sync_client_ptr, limit, ctypes.byref(outgoing_message_count)) return outgoing_message_count.value + + +class Sync: + __sync_clients: dict[Store, SyncClient] = {} + + @staticmethod + def is_available() -> bool: + return c.obx_has_feature(c.Feature.Sync) + + @staticmethod + def client( + store: Store, + server_url: str, + credential: SyncCredentials, + filter_variables: dict[str, str] | None = None + ) -> SyncClient: + client = SyncClient(store, [server_url], filter_variables) + client.set_credentials(credential) + return client + + @staticmethod + def client_multi_creds( + store: Store, + server_url: str, + credentials_list: list[SyncCredentials], + filter_variables: dict[str, str] | None = None + ) -> SyncClient: + client = SyncClient(store, [server_url], filter_variables) + client.set_multiple_credentials(credentials_list) + return client + + @staticmethod + def client_multi_urls( + store: Store, + server_urls: list[str], + credential: SyncCredentials, + filter_variables: dict[str, str] | None = None + ) -> SyncClient: + client = SyncClient(store, server_urls, filter_variables) + client.set_credentials(credential) + return client + + @staticmethod + def client_multi_creds_multi_urls( + store: Store, + server_urls: list[str], + credentials_list: list[SyncCredentials], + filter_variables: dict[str, str] | None = None + ) -> SyncClient: + if store in Sync.__sync_clients: + raise ValueError('Only one sync client can be active for a store') + client = SyncClient(store, server_urls, filter_variables) + client.set_multiple_credentials(credentials_list) + Sync.__sync_clients[store] = client + return client From b75bb850a460ce9f4d6fe4e7fd9521244245ffa6 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 11:27:11 +0530 Subject: [PATCH 11/21] Remove .values() from Store.py when invoking store close listeners --- objectbox/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/objectbox/store.py b/objectbox/store.py index 3470bc8..2fb07ba 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -273,7 +273,7 @@ def write_tx(self): def close(self): """Close database.""" - for listener in self._close_listeners.values(): + for listener in self._close_listeners: listener() self._close_listeners.clear() c_store_to_close = self._c_store From b59e78674f0acf2293870842afdf4425654edddb Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 11:28:00 +0530 Subject: [PATCH 12/21] Check Sync available when constructing SyncClient instance --- objectbox/sync.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/objectbox/sync.py b/objectbox/sync.py index 16050b9..1ebeecf 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -143,11 +143,10 @@ def __init__(self, store: Store, server_urls: list[str], if not server_urls: raise ValueError("Provide at least one server URL") - # TODO: Implement sync availability check - # if not c.Sync.is_available(): - # raise RuntimeError( - # 'Sync is not available in the loaded ObjectBox runtime library. ' - # 'Please visit https://objectbox.io/sync/ for options.') + if not Sync.is_available(): + raise RuntimeError( + 'Sync is not available in the loaded ObjectBox runtime library. ' + 'Please visit https://objectbox.io/sync/ for options.') self.__store = store self.__server_urls = [url.encode('utf-8') for url in server_urls] From 04d5a36ff3123364cd8677fcc1fbea00e97917f9 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 25 Dec 2025 11:31:48 +0530 Subject: [PATCH 13/21] Add filter variables when creating an instance of SyncClient --- objectbox/sync.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/objectbox/sync.py b/objectbox/sync.py index 1ebeecf..f516ce0 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -155,6 +155,9 @@ def __init__(self, store: Store, server_urls: list[str], c.c_array_pointer(self.__server_urls, ctypes.c_char_p), len(self.__server_urls)) + for name, value in (filter_variables or {}).items(): + self.add_filter_variable(name, value) + self.__store.add_store_close_listener(on_store_close=self.__close_sync_client_func()) def __close_sync_client_func(self): From 7be070b9efa8af53b6e2acea2414156889382337 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sun, 28 Dec 2025 09:15:25 +0530 Subject: [PATCH 14/21] Add requestUpdates() and cancelUpdates() methods --- objectbox/c.py | 6 ++++++ objectbox/sync.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/objectbox/c.py b/objectbox/c.py index aaf84df..a61dbcb 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -1259,3 +1259,9 @@ class Feature(IntEnum): obx_has_feature = c_fn('obx_has_feature', ctypes.c_bool, [OBXFeature]) + +# OBX_C_API obx_err obx_sync_updates_request(OBX_sync* sync, bool subscribe_for_pushes); +obx_sync_updates_request = c_fn_rc('obx_sync_updates_request', [OBX_sync_p, ctypes.c_bool]) + +# OBX_C_API obx_err obx_sync_updates_cancel(OBX_sync* sync); +obx_sync_updates_cancel = c_fn_rc('obx_sync_updates_cancel', [OBX_sync_p]) diff --git a/objectbox/sync.py b/objectbox/sync.py index f516ce0..cf3565e 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -245,6 +245,12 @@ def stop(self): def trigger_reconnect(self) -> bool: return c.check_obx_success(c.obx_sync_trigger_reconnect(self.__c_sync_client_ptr)) + def request_updates(self, subscribe_for_future_pushes: bool) -> bool: + return c.check_obx_success(c.obx_sync_updates_request(self.__c_sync_client_ptr, subscribe_for_future_pushes)) + + def cancel_updates(self) -> bool: + return c.check_obx_success(c.obx_sync_updates_cancel(self.__c_sync_client_ptr)) + @staticmethod def protocol_version() -> int: return c.obx_sync_protocol_version() From 66f708b5fa76384ab64cd10541e4e4e69b06f712 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sun, 28 Dec 2025 10:41:32 +0530 Subject: [PATCH 15/21] add pre-check for sync client ptr not null --- objectbox/sync.py | 21 +++++++++++++++++++++ tests/test_sync.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/objectbox/sync.py b/objectbox/sync.py index cf3565e..4027e5a 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -166,7 +166,12 @@ def close_sync_client(): return close_sync_client + def __check_sync_ptr_not_null(self): + if self.__c_sync_client_ptr is None: + raise ValueError('SyncClient already closed') + def set_credentials(self, credentials: SyncCredentials): + self.__check_sync_ptr_not_null() self.__credentials = credentials if isinstance(credentials, SyncCredentialsNone): c.obx_sync_credentials(self.__c_sync_client_ptr, credentials.type, None, 0) @@ -181,6 +186,7 @@ def set_credentials(self, credentials: SyncCredentials): len(credentials.secret)) def set_multiple_credentials(self, credentials_list: list[SyncCredentials]): + self.__check_sync_ptr_not_null() if len(credentials_list) == 0: raise ValueError("Provide at least one credential") @@ -207,6 +213,7 @@ def set_multiple_credentials(self, credentials_list: list[SyncCredentials]): def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): + self.__check_sync_ptr_not_null() if mode == SyncRequestUpdatesMode.MANUAL: c_mode = c.RequestUpdatesMode.MANUAL elif mode == SyncRequestUpdatesMode.AUTO: @@ -218,6 +225,7 @@ def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): c.obx_sync_request_updates_mode(self.__c_sync_client_ptr, c_mode) def get_sync_state(self) -> SyncState: + self.__check_sync_ptr_not_null() c_state = c.obx_sync_state(self.__c_sync_client_ptr) if c_state == c.SyncState.CREATED: return SyncState.CREATED @@ -237,18 +245,23 @@ def get_sync_state(self) -> SyncState: return SyncState.UNKNOWN def start(self): + self.__check_sync_ptr_not_null() c.obx_sync_start(self.__c_sync_client_ptr) def stop(self): + self.__check_sync_ptr_not_null() c.obx_sync_stop(self.__c_sync_client_ptr) def trigger_reconnect(self) -> bool: + self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_trigger_reconnect(self.__c_sync_client_ptr)) def request_updates(self, subscribe_for_future_pushes: bool) -> bool: + self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_updates_request(self.__c_sync_client_ptr, subscribe_for_future_pushes)) def cancel_updates(self) -> bool: + self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_updates_cancel(self.__c_sync_client_ptr)) @staticmethod @@ -266,6 +279,7 @@ def is_closed(self) -> bool: return self.__c_sync_client_ptr is None def set_login_listener(self, login_listener: SyncLoginListener): + self.__check_sync_ptr_not_null() self.__c_login_listener = c.OBX_sync_listener_login(lambda arg: login_listener.on_logged_in()) self.__c_login_failure_listener = c.OBX_sync_listener_login_failure( lambda arg, sync_login_code: login_listener.on_login_failed(sync_login_code)) @@ -281,6 +295,7 @@ def set_login_listener(self, login_listener: SyncLoginListener): ) def set_connection_listener(self, connection_listener: SyncConnectionListener): + self.__check_sync_ptr_not_null() self.__c_connect_listener = c.OBX_sync_listener_connect(lambda arg: connection_listener.on_connected()) self.__c_disconnect_listener = c.OBX_sync_listener_disconnect(lambda arg: connection_listener.on_disconnected()) c.obx_sync_listener_connect( @@ -295,6 +310,7 @@ def set_connection_listener(self, connection_listener: SyncConnectionListener): ) def set_error_listener(self, error_listener: SyncErrorListener): + self.__check_sync_ptr_not_null() self.__c_error_listener = c.OBX_sync_listener_error( lambda arg, sync_error_code: error_listener.on_error(sync_error_code)) c.obx_sync_listener_error( @@ -304,18 +320,23 @@ def set_error_listener(self, error_listener: SyncErrorListener): ) def wait_for_logged_in_state(self, timeout_millis: int): + self.__check_sync_ptr_not_null() c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis) def add_filter_variable(self, name: str, value: str): + self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_put(self.__c_sync_client_ptr, name.encode('utf-8'), value.encode('utf-8')) def remove_filter_variable(self, name: str): + self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_remove(self.__c_sync_client_ptr, name.encode('utf-8')) def remove_all_filter_variables(self): + self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_remove_all(self.__c_sync_client_ptr) def get_outgoing_message_count(self, limit: int = 0) -> int: + self.__check_sync_ptr_not_null() outgoing_message_count = ctypes.c_uint64(0) c.obx_sync_outgoing_message_count(self.__c_sync_client_ptr, limit, ctypes.byref(outgoing_message_count)) return outgoing_message_count.value diff --git a/tests/test_sync.py b/tests/test_sync.py index 9534df7..f5a5b39 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from time import sleep import pytest @@ -104,3 +105,32 @@ def test_client_closed_when_store_closed(test_store): assert not client.is_closed() test_store.close() assert client.is_closed() + + +def assert_raises_value_error(fn: Callable[[], object | None], message: str | None = None): + with pytest.raises(ValueError, match=message): + fn() + + +def test_client_access_after_close_throws_error(test_store): + server_urls = ["ws://localhost:9999"] + client = SyncClient(test_store, server_urls) + client.close() + + assert client.is_closed() + + match_error = "SyncClient already closed" + + assert_raises_value_error(message=match_error, fn=lambda: client.start()) + assert_raises_value_error(message=match_error, fn=lambda: client.stop()) + assert_raises_value_error(message=match_error, fn=lambda: client.get_sync_state()) + assert_raises_value_error(message=match_error, fn=lambda: client.get_outgoing_message_count()) + assert_raises_value_error(message=match_error, fn=lambda: client.set_credentials(SyncCredentials.none())) + assert_raises_value_error(message=match_error, + fn=lambda: client.set_credentials(SyncCredentials.google_auth("token_google"))) + assert_raises_value_error(message=match_error, fn=lambda: client.set_multiple_credentials([ + SyncCredentials.google_auth("token_google"), + SyncCredentials.user_and_password("user1", "password") + ])) + assert_raises_value_error(message=match_error, + fn=lambda: client.set_request_updates_mode(SyncRequestUpdatesMode.AUTO)) From e70fa2e5b0f3795ebf1d9c82670163f2f6bcf960 Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 30 Dec 2025 13:07:21 +0530 Subject: [PATCH 16/21] Allow building different package for OBX Sync Include Sync libraries when building Sync package --- Makefile | 9 +++++++++ download-c-lib.py | 5 ++++- setup.py | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 579e6f4..642a223 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,11 @@ build: ${VENV} clean ## Clean and build ${PYTHON} setup.py bdist_wheel ; \ ls -lh dist +build-sync: ${VENV} clean ## Clean and build + set -e ; \ + OBX_BUILD_SYNC=1 ${PYTHON} setup.py bdist_wheel ; \ + ls -lh dist + ${VENV}: ${VENVBIN}/activate venv-init: @@ -49,6 +54,10 @@ depend: ${VENV} ## Prepare dependencies set -e ; \ ${PYTHON} download-c-lib.py +depend-sync: ${VENV} ## Prepare dependencies + set -e ; \ + ${PYTHON} download-c-lib.py --sync + test: ${VENV} ## Test all targets set -e ; \ ${PYTHON} -m pytest --capture=no --verbose diff --git a/download-c-lib.py b/download-c-lib.py index 9e1dedf..b75bfaf 100644 --- a/download-c-lib.py +++ b/download-c-lib.py @@ -2,12 +2,15 @@ import tarfile import zipfile import os +import sys # Script used to download objectbox-c shared libraries for all supported platforms. Execute by running `make get-lib` # on first checkout of this repo and any time after changing the objectbox-c lib version. version = "v5.0.0" # see objectbox/c.py required_version -variant = 'objectbox-sync' # or 'objectbox-sync' +variant = 'objectbox' # or 'objectbox-sync' +if len(sys.argv) > 1 and sys.argv[1] == '--sync': + variant = 'objectbox-sync' base_url = "https://github.com/objectbox/objectbox-c/releases/download/" diff --git a/setup.py b/setup.py index bda3122..b328c9f 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,17 @@ +import os + import setuptools import objectbox with open("README.md", "r") as fh: long_description = fh.read() +package_name = "objectbox" +if "OBX_BUILD_SYNC" in os.environ: + package_name = "objectbox-sync" + setuptools.setup( - name="objectbox", + name=package_name, version=str(objectbox.version), author="ObjectBox", description="ObjectBox is a superfast lightweight database for objects", From 8d6023c4e160737fe7e008ba77dfd25e076fd0e9 Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 30 Dec 2025 13:08:51 +0530 Subject: [PATCH 17/21] Set version to 5.0.0 in package's __init__.py --- objectbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 71bf846..5a59694 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -78,7 +78,7 @@ ] # Python binding version -version = Version(4, 0, 0) +version = Version(5, 0, 0) """ObjectBox Python package version""" def version_info(): From 1ba8f1633eef03d845be33a2a3dc9938dbbba057 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 31 Dec 2025 13:14:20 +0530 Subject: [PATCH 18/21] Allow adding entity flags for Sync --- objectbox/__init__.py | 5 +++-- objectbox/c.py | 8 ++++++++ objectbox/model/entity.py | 7 +++++++ objectbox/model/model.py | 1 + 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 5a59694..d9a1b92 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -16,7 +16,7 @@ from objectbox.store import Store from objectbox.box import Box -from objectbox.model.entity import Entity +from objectbox.model.entity import Entity, SyncEntity from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model from objectbox.c import version_core, DebugFlags @@ -74,7 +74,8 @@ 'PropertyQueryCondition', 'HnswFlags', 'Query', - 'QueryBuilder' + 'QueryBuilder', + 'SyncEntity' ] # Python binding version diff --git a/objectbox/c.py b/objectbox/c.py index a61dbcb..1fb0625 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -414,6 +414,11 @@ class DbErrorCode(IntEnum): OBX_ERROR_TREE_OTHER = 10699 +class OBXEntityFlags(IntEnum): + SYNC_ENABLED = 2 + SHARED_GLOBAL_IDS = 4 + + def check_obx_err(code: obx_err, func, args) -> obx_err: """ Raises an exception if obx_err is not successful. """ if code != DbErrorCode.OBX_SUCCESS: @@ -535,6 +540,9 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_model_entity = c_fn_rc('obx_model_entity', [ OBX_model_p, ctypes.c_char_p, obx_schema_id, obx_uid]) +# obx_err obx_model_entity_flags(OBX_model* model, uint32_t flags); +obx_model_entity_flags = c_fn_rc('obx_model_entity_flags', [OBX_model_p, ctypes.c_uint32]) + # obx_err (OBX_model* model, const char* name, OBXPropertyType type, obx_schema_id property_id, obx_uid property_uid); obx_model_property = c_fn_rc('obx_model_property', [OBX_model_p, ctypes.c_char_p, OBXPropertyType, obx_schema_id, obx_uid]) diff --git a/objectbox/model/entity.py b/objectbox/model/entity.py index 9e6d46a..dda6f62 100644 --- a/objectbox/model/entity.py +++ b/objectbox/model/entity.py @@ -38,6 +38,7 @@ def __init__(self, user_type, uid: int = 0): self._id_property = None self._fill_properties() self._tl = threading.local() + self._flags = 0 @property def _id(self) -> int: @@ -320,3 +321,9 @@ def wrapper(class_) -> Callable[[Type], _Entity]: return entity_type return wrapper + + +def SyncEntity(cls): + entity: _Entity = obx_models_by_name["default"][-1] # get the last added entity + entity._flags |= OBXEntityFlags.SYNC_ENABLED + return cls diff --git a/objectbox/model/model.py b/objectbox/model/model.py index c4e4875..2c75ad5 100644 --- a/objectbox/model/model.py +++ b/objectbox/model/model.py @@ -102,6 +102,7 @@ def _create_property(self, prop: Property): def _create_entity(self, entity: _Entity): obx_model_entity(self._c_model, c_str(entity._name), entity._id, entity._uid) + obx_model_entity_flags(self._c_model, entity._flags) for prop in entity._properties: self._create_property(prop) obx_model_entity_last_property_id(self._c_model, entity._last_property_iduid.id, entity._last_property_iduid.uid) From ea8db2ee51be2025ff33eb49d464e6c86e3aa1cb Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 31 Dec 2025 18:20:18 +0530 Subject: [PATCH 19/21] Document C functions in c.py --- objectbox/c.py | 320 ++++++++++++++++++++++++++++--------------------- 1 file changed, 182 insertions(+), 138 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 1fb0625..141ca06 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -253,115 +253,6 @@ class OBX_query(ctypes.Structure): OBX_query_p = ctypes.POINTER(OBX_query) - -# Sync types -class OBX_sync(ctypes.Structure): - pass - - -OBX_sync_p = ctypes.POINTER(OBX_sync) - - -class OBX_sync_server(ctypes.Structure): - pass - - -OBX_sync_server_p = ctypes.POINTER(OBX_sync_server) - -OBXSyncCredentialsType = ctypes.c_int -OBXRequestUpdatesMode = ctypes.c_int -OBXSyncState = ctypes.c_int -OBXSyncCode = ctypes.c_int - -class SyncCredentialsType(IntEnum): - NONE = 1 - SHARED_SECRET = 2 # Deprecated, use SHARED_SECRET_SIPPED instead - GOOGLE_AUTH = 3 - SHARED_SECRET_SIPPED = 4 # Uses shared secret to create a hashed credential - OBX_ADMIN_USER = 5 # ObjectBox admin users (username/password) - USER_PASSWORD = 6 # Generic credential type for admin users - JWT_ID = 7 # JSON Web Token (JWT): ID token with user identity - JWT_ACCESS = 8 # JSON Web Token (JWT): access token for resources - JWT_REFRESH = 9 # JSON Web Token (JWT): refresh token - JWT_CUSTOM = 10 # JSON Web Token (JWT): custom token type - - -class RequestUpdatesMode(IntEnum): - MANUAL = 0 # No updates by default, must call obx_sync_updates_request() manually - AUTO = 1 # Same as calling obx_sync_updates_request(sync, TRUE) - AUTO_NO_PUSHES = 2 # Same as calling obx_sync_updates_request(sync, FALSE) - - -class SyncState(IntEnum): - CREATED = 1 - STARTED = 2 - CONNECTED = 3 - LOGGED_IN = 4 - DISCONNECTED = 5 - STOPPED = 6 - DEAD = 7 - -class OBXSyncError(IntEnum): - REJECT_TX_NO_PERMISSION = 1 # Sync client received rejection of transaction writes due to missing permissions - - -class OBXSyncObjectType(IntEnum): - FlatBuffers = 1 - String = 2 - Raw = 3 - - -class OBX_sync_change(ctypes.Structure): - _fields_ = [ - ('entity_id', obx_schema_id), - ('puts', ctypes.POINTER(OBX_id_array)), - ('removals', ctypes.POINTER(OBX_id_array)), - ] - - -class OBX_sync_change_array(ctypes.Structure): - _fields_ = [ - ('list', ctypes.POINTER(OBX_sync_change)), - ('count', ctypes.c_size_t), - ] - - -class OBX_sync_object(ctypes.Structure): - _fields_ = [ - ('type', ctypes.c_int), # OBXSyncObjectType - ('id', ctypes.c_uint64), - ('data', ctypes.c_void_p), - ('size', ctypes.c_size_t), - ] - - -class OBX_sync_msg_objects(ctypes.Structure): - _fields_ = [ - ('topic', ctypes.c_void_p), - ('topic_size', ctypes.c_size_t), - ('objects', ctypes.POINTER(OBX_sync_object)), - ('count', ctypes.c_size_t), - ] - - -class OBX_sync_msg_objects_builder(ctypes.Structure): - pass - - -OBX_sync_msg_objects_builder_p = ctypes.POINTER(OBX_sync_msg_objects_builder) - -# Define callback types for sync listeners -OBX_sync_listener_connect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -OBX_sync_listener_disconnect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -OBX_sync_listener_login = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -OBX_sync_listener_login_failure = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncCode -OBX_sync_listener_complete = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -OBX_sync_listener_error = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncError -OBX_sync_listener_change = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_change_array)) -OBX_sync_listener_server_time = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64) -OBX_sync_listener_msg_objects = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_msg_objects)) - - # manually configure error methods, we can't use `fn()` defined below yet due to circular dependencies C.obx_last_error_message.restype = ctypes.c_char_p C.obx_last_error_code.restype = obx_err @@ -1193,47 +1084,206 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): OBXBackupRestoreFlags_None = 0 OBXBackupRestoreFlags_OverwriteExistingData = 1 + +# Sync API + +class OBX_sync(ctypes.Structure): + pass + + +OBX_sync_p = ctypes.POINTER(OBX_sync) + + +class OBX_sync_server(ctypes.Structure): + pass + + +OBX_sync_server_p = ctypes.POINTER(OBX_sync_server) + +OBXSyncCredentialsType = ctypes.c_int +OBXRequestUpdatesMode = ctypes.c_int +OBXSyncState = ctypes.c_int +OBXSyncCode = ctypes.c_int + + +class SyncCredentialsType(IntEnum): + NONE = 1 + SHARED_SECRET = 2 # Deprecated, use SHARED_SECRET_SIPPED instead + GOOGLE_AUTH = 3 + SHARED_SECRET_SIPPED = 4 # Uses shared secret to create a hashed credential + OBX_ADMIN_USER = 5 # ObjectBox admin users (username/password) + USER_PASSWORD = 6 # Generic credential type for admin users + JWT_ID = 7 # JSON Web Token (JWT): ID token with user identity + JWT_ACCESS = 8 # JSON Web Token (JWT): access token for resources + JWT_REFRESH = 9 # JSON Web Token (JWT): refresh token + JWT_CUSTOM = 10 # JSON Web Token (JWT): custom token type + + +class RequestUpdatesMode(IntEnum): + MANUAL = 0 # No updates by default, must call obx_sync_updates_request() manually + AUTO = 1 # Same as calling obx_sync_updates_request(sync, TRUE) + AUTO_NO_PUSHES = 2 # Same as calling obx_sync_updates_request(sync, FALSE) + + +class SyncState(IntEnum): + CREATED = 1 + STARTED = 2 + CONNECTED = 3 + LOGGED_IN = 4 + DISCONNECTED = 5 + STOPPED = 6 + DEAD = 7 + + +class OBXSyncError(IntEnum): + REJECT_TX_NO_PERMISSION = 1 # Sync client received rejection of transaction writes due to missing permissions + + +class OBXSyncObjectType(IntEnum): + FlatBuffers = 1 + String = 2 + Raw = 3 + + +class OBX_sync_change(ctypes.Structure): + _fields_ = [ + ('entity_id', obx_schema_id), + ('puts', ctypes.POINTER(OBX_id_array)), + ('removals', ctypes.POINTER(OBX_id_array)), + ] + + +class OBX_sync_change_array(ctypes.Structure): + _fields_ = [ + ('list', ctypes.POINTER(OBX_sync_change)), + ('count', ctypes.c_size_t), + ] + + +class OBX_sync_object(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_int), # OBXSyncObjectType + ('id', ctypes.c_uint64), + ('data', ctypes.c_void_p), + ('size', ctypes.c_size_t), + ] + + +class OBX_sync_msg_objects(ctypes.Structure): + _fields_ = [ + ('topic', ctypes.c_void_p), + ('topic_size', ctypes.c_size_t), + ('objects', ctypes.POINTER(OBX_sync_object)), + ('count', ctypes.c_size_t), + ] + + +class OBX_sync_msg_objects_builder(ctypes.Structure): + pass + + +OBX_sync_msg_objects_builder_p = ctypes.POINTER(OBX_sync_msg_objects_builder) + +# Define callback types for sync listeners +OBX_sync_listener_connect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_disconnect = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_login = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_login_failure = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncCode +OBX_sync_listener_complete = ctypes.CFUNCTYPE(None, ctypes.c_void_p) +OBX_sync_listener_error = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int) # arg, OBXSyncError +OBX_sync_listener_change = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_change_array)) +OBX_sync_listener_server_time = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64) +OBX_sync_listener_msg_objects = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(OBX_sync_msg_objects)) + +# OBX_sync* obx_sync(OBX_store* store, const char* server_url); obx_sync = c_fn("obx_sync", OBX_sync_p, [OBX_store_p, ctypes.c_char_p]) + +# OBX_sync* obx_sync_urls(OBX_store* store, const char* server_urls[], size_t server_urls_count); obx_sync_urls = c_fn("obx_sync_urls", OBX_sync_p, [OBX_store_p, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t]) +# Client Credentials +# obx_err obx_sync_credentials(OBX_sync* sync, OBXSyncCredentialsType type, const void* data, size_t size); obx_sync_credentials = c_fn_rc('obx_sync_credentials', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t]) + +# obx_err obx_sync_credentials_user_password(OBX_sync* sync, OBXSyncCredentialsType type, const char* username, const char* password); obx_sync_credentials_user_password = c_fn_rc('obx_sync_credentials_user_password', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, ctypes.c_char_p]) + +# obx_err obx_sync_credentials_add(OBX_sync* sync, OBXSyncCredentialsType type, const void* data, size_t size, bool complete); obx_sync_credentials_add = c_fn_rc('obx_sync_credentials_add', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_bool]) + +# obx_err obx_sync_credentials_add_user_password(OBX_sync* sync, OBXSyncCredentialsType type, const char* username, const char* password, bool complete); obx_sync_credentials_add_user_password = c_fn_rc('obx_sync_credentials_add_user_password', [OBX_sync_p, OBXSyncCredentialsType, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]) +# Sync Control + +# OBXSyncState obx_sync_state(OBX_sync* sync); obx_sync_state = c_fn('obx_sync_state', OBXSyncState, [OBX_sync_p]) +# obx_err obx_sync_request_updates_mode(OBX_sync* sync, OBXRequestUpdatesMode mode); obx_sync_request_updates_mode = c_fn_rc('obx_sync_request_updates_mode', [OBX_sync_p, OBXRequestUpdatesMode]) +# OBX_C_API obx_err obx_sync_updates_request(OBX_sync* sync, bool subscribe_for_pushes); +obx_sync_updates_request = c_fn_rc('obx_sync_updates_request', [OBX_sync_p, ctypes.c_bool]) + +# OBX_C_API obx_err obx_sync_updates_cancel(OBX_sync* sync); +obx_sync_updates_cancel = c_fn_rc('obx_sync_updates_cancel', [OBX_sync_p]) + +# obx_err obx_sync_start(OBX_sync* sync); obx_sync_start = c_fn_rc('obx_sync_start', [OBX_sync_p]) + +# obx_err obx_sync_stop(OBX_sync* sync); obx_sync_stop = c_fn_rc('obx_sync_stop', [OBX_sync_p]) +# obx_err obx_sync_trigger_reconnect(OBX_sync* sync); obx_sync_trigger_reconnect = c_fn_rc('obx_sync_trigger_reconnect', [OBX_sync_p]) +# uint32_t obx_sync_protocol_version(); obx_sync_protocol_version = c_fn('obx_sync_protocol_version', ctypes.c_uint32, []) + +# uint32_t obx_sync_protocol_version_server(OBX_sync* sync); obx_sync_protocol_version_server = c_fn('obx_sync_protocol_version_server', ctypes.c_uint32, [OBX_sync_p]) +# obx_err obx_sync_wait_for_logged_in_state(OBX_sync* sync, uint64_t timeout_millis); +obx_sync_wait_for_logged_in_state = c_fn_rc('obx_sync_wait_for_logged_in_state', [OBX_sync_p, ctypes.c_uint64]) + +# obx_err obx_sync_close(OBX_sync* sync); obx_sync_close = c_fn_rc('obx_sync_close', [OBX_sync_p]) +# Listener Callbacks + +# void obx_sync_listener_connect(OBX_sync* sync, OBX_sync_listener_connect* listener, void* listener_arg); obx_sync_listener_connect = c_fn('obx_sync_listener_connect', None, [OBX_sync_p, OBX_sync_listener_connect, ctypes.c_void_p]) + +# void obx_sync_listener_disconnect(OBX_sync* sync, OBX_sync_listener_disconnect* listener, void* listener_arg); obx_sync_listener_disconnect = c_fn('obx_sync_listener_disconnect', None, [OBX_sync_p, OBX_sync_listener_disconnect, ctypes.c_void_p]) + +# void obx_sync_listener_login(OBX_sync* sync, OBX_sync_listener_login* listener, void* listener_arg); obx_sync_listener_login = c_fn('obx_sync_listener_login', None, [OBX_sync_p, OBX_sync_listener_login, ctypes.c_void_p]) + +# void obx_sync_listener_login_failure(OBX_sync* sync, OBX_sync_listener_login_failure* listener, void* listener_arg); obx_sync_listener_login_failure = c_fn('obx_sync_listener_login_failure', None, [OBX_sync_p, OBX_sync_listener_login_failure, ctypes.c_void_p]) + +# void obx_sync_listener_complete(OBX_sync* sync, OBX_sync_listener_complete* listener, void* listener_arg); obx_sync_listener_error = c_fn('obx_sync_listener_error', None, [OBX_sync_p, OBX_sync_listener_error, ctypes.c_void_p]) -obx_sync_wait_for_logged_in_state = c_fn_rc('obx_sync_wait_for_logged_in_state', [OBX_sync_p, ctypes.c_uint64]) +# Filter Variables +# obx_err obx_sync_filter_variables_put(OBX_sync* sync, const char* name, const char* value); obx_sync_filter_variables_put = c_fn_rc('obx_sync_filter_variables_put', [OBX_sync_p, c_char_p, c_char_p]) + +# obx_err obx_sync_filter_variables_remove(OBX_sync* sync, const char* name); obx_sync_filter_variables_remove = c_fn_rc('obx_sync_filter_variables_remove', [OBX_sync_p, c_char_p]) + +# obx_err obx_sync_filter_variables_remove_all(OBX_sync* sync); obx_sync_filter_variables_remove_all = c_fn_rc('obx_sync_filter_variables_remove_all', [OBX_sync_p]) @@ -1243,33 +1293,27 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): OBXFeature = ctypes.c_int - class Feature(IntEnum): - ResultArray = 1 - TimeSeries = 2 - Sync = 3 - DebugLog = 4 - Admin = 5 - Tree = 6 - SyncServer = 7 - WebSockets = 8 - Cluster = 9 - HttpServer = 10 - GraphQL = 11 - Backup = 12 - Lmdb = 13 - VectorSearch = 14 - Wal = 15 - SyncMongoDb = 16 - Auth = 17 - Trial = 18 - SyncFilters = 19 - - + ResultArray = 1 # Functions that are returning multiple results (e.g. multiple objects) can be only used if this is available. + TimeSeries = 2 # TimeSeries support (date/date-nano companion ID and other time-series functionality). + Sync = 3 # Sync client availability. Visit https://objectbox.io/sync for more details. + DebugLog = 4 # Check whether debug log can be enabled during runtime. + Admin = 5 # Admin UI including a database browser, user management, and more. Depends on HttpServer (if Admin is available HttpServer is too). + Tree = 6 # Tree with special GraphQL support + SyncServer = 7 # Sync server availability. Visit https://objectbox.io/sync for more details. + WebSockets = 8 # Implicitly added by Sync or SyncServer; disable via NoWebSockets + Cluster = 9 # Sync Server has cluster functionality. Implicitly added by SyncServer; disable via NoCluster + HttpServer = 10 # Embedded HTTP server. + GraphQL = 11 # Embedded GraphQL server (via HTTP). Depends on HttpServer (if GraphQL is available HttpServer is too). + Backup = 12 # Database Backup functionality; typically only enabled in Sync Server builds. + Lmdb = 13 # The default database "provider"; writes data persistently to disk (ACID). + VectorSearch = 14 # Vector search functionality; enables indexing for nearest neighbor search. + Wal = 15 # WAL (write-ahead logging). + SyncMongoDb = 16 # Sync connector to integrate MongoDB with SyncServer. + Auth = 17 # Enables additional authentication/authorization methods for sync login, e.g. + Trial = 18 # This is a free trial version; only applies to server builds (no trial builds for database and Sync clients). + SyncFilters = 19 # Server-side filters to return individual data for each sync user (user-specific data). + + +# bool obx_has_feature(OBXFeature feature); obx_has_feature = c_fn('obx_has_feature', ctypes.c_bool, [OBXFeature]) - -# OBX_C_API obx_err obx_sync_updates_request(OBX_sync* sync, bool subscribe_for_pushes); -obx_sync_updates_request = c_fn_rc('obx_sync_updates_request', [OBX_sync_p, ctypes.c_bool]) - -# OBX_C_API obx_err obx_sync_updates_cancel(OBX_sync* sync); -obx_sync_updates_cancel = c_fn_rc('obx_sync_updates_cancel', [OBX_sync_p]) From baf6d9c49702bd5fcd7a52b13d99ae1595aeea4a Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 31 Dec 2025 18:32:59 +0530 Subject: [PATCH 20/21] Document classes/methods in sync.py --- objectbox/sync.py | 410 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) diff --git a/objectbox/sync.py b/objectbox/sync.py index 4027e5a..c176982 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -5,136 +5,337 @@ from objectbox import Store class SyncCredentials: + """Credentials used to authenticate a sync client against a server.""" def __init__(self, credential_type: c.SyncCredentialsType): self.type = credential_type @staticmethod def none() -> 'SyncCredentials': + """No credentials - usually only for development purposes with a server + configured to accept all connections without authentication. + + Returns: + A SyncCredentials instance with no authentication. + """ return SyncCredentialsNone() @staticmethod def shared_secret_string(secret: str) -> 'SyncCredentials': + """Shared secret authentication. + + Args: + secret: The shared secret string. + + Returns: + A SyncCredentials instance for shared secret authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.SHARED_SECRET_SIPPED, secret.encode('utf-8')) @staticmethod def google_auth(secret: str) -> 'SyncCredentials': + """Google authentication. + + Args: + secret: The Google authentication token. + + Returns: + A SyncCredentials instance for Google authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.GOOGLE_AUTH, secret.encode('utf-8')) @staticmethod def user_and_password(username: str, password: str) -> 'SyncCredentials': + """Username and password authentication. + + Args: + username: The username. + password: The password. + + Returns: + A SyncCredentials instance for username/password authentication. + """ return SyncCredentialsUserPassword(c.SyncCredentialsType.USER_PASSWORD, username, password) @staticmethod def jwt_id_token(jwt_id_token: str) -> 'SyncCredentials': + """JSON Web Token (JWT): an ID token that typically provides identity + information about the authenticated user. + + Args: + jwt_id_token: The JWT ID token. + + Returns: + A SyncCredentials instance for JWT ID token authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ID, jwt_id_token.encode('utf-8')) @staticmethod def jwt_access_token(jwt_access_token: str) -> 'SyncCredentials': + """JSON Web Token (JWT): an access token that is used to access resources. + + Args: + jwt_access_token: The JWT access token. + + Returns: + A SyncCredentials instance for JWT access token authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ACCESS, jwt_access_token.encode('utf-8')) @staticmethod def jwt_refresh_token(jwt_refresh_token: str) -> 'SyncCredentials': + """JSON Web Token (JWT): a refresh token that is used to obtain a new + access token. + + Args: + jwt_refresh_token: The JWT refresh token. + + Returns: + A SyncCredentials instance for JWT refresh token authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_REFRESH, jwt_refresh_token.encode('utf-8')) @staticmethod def jwt_custom_token(jwt_custom_token: str) -> 'SyncCredentials': + """JSON Web Token (JWT): a token that is neither an ID, access, + nor refresh token. + + Args: + jwt_custom_token: The custom JWT token. + + Returns: + A SyncCredentials instance for custom JWT token authentication. + """ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_CUSTOM, jwt_custom_token.encode('utf-8')) class SyncCredentialsNone(SyncCredentials): + """Internal use only. Represents no credentials for authentication.""" + def __init__(self): super().__init__(c.SyncCredentialsType.NONE) class SyncCredentialsSecret(SyncCredentials): + """Internal use only. Sync credential that is a single secret string.""" + def __init__(self, credential_type: c.SyncCredentialsType, secret: bytes): + """Creates a secret-based credential. + + Args: + credential_type: The type of credential. + secret: UTF-8 encoded secret bytes. + """ super().__init__(credential_type) self.secret = secret class SyncCredentialsUserPassword(SyncCredentials): + """Internal use only. Sync credential with username and password.""" + def __init__(self, credential_type: c.SyncCredentialsType, username: str, password: str): + """Creates a username/password credential. + + Args: + credential_type: The type of credential. + username: The username. + password: The password. + """ super().__init__(credential_type) self.username = username self.password = password class SyncState(Enum): + """Current state of the SyncClient.""" + UNKNOWN = auto() + """State is unknown, e.g. C-API reported a state that's not recognized yet.""" + CREATED = auto() + """Client created but not yet started.""" + STARTED = auto() + """Client started and connecting.""" + CONNECTED = auto() + """Connection with the server established but not authenticated yet.""" + LOGGED_IN = auto() + """Client authenticated and synchronizing.""" + DISCONNECTED = auto() + """Lost connection, will try to reconnect if the credentials are valid.""" + STOPPED = auto() + """Client in the process of being closed.""" + DEAD = auto() + """Invalid access to the client after it was closed.""" class SyncRequestUpdatesMode: + """Configuration of how SyncClient fetches remote updates from the server.""" + MANUAL = 'manual' + """No updates, SyncClient.request_updates() must be called manually.""" + AUTO = 'auto' + """Automatic updates, including subsequent pushes from the server, same as + calling SyncClient.request_updates(True). This is the default unless + changed by SyncClient.set_request_updates_mode().""" + AUTO_NO_PUSHES = 'auto_no_pushes' + """Automatic update after connection, without subscribing for pushes from the + server. Similar to calling SyncClient.request_updates(False).""" class SyncConnectionEvent: + """Connection state change event.""" + CONNECTED = 'connected' + """Connection to the server is established.""" + DISCONNECTED = 'disconnected' + """Connection to the server is lost.""" class SyncLoginEvent: + """Login state change event.""" + LOGGED_IN = 'logged_in' + """Client has successfully logged in to the server.""" + CREDENTIALS_REJECTED = 'credentials_rejected' + """Client's credentials have been rejected by the server. + Connection will NOT be retried until new credentials are provided.""" + UNKNOWN_ERROR = 'unknown_error' + """An unknown error occurred during authentication.""" class SyncCode(IntEnum): + """Sync response/error codes.""" + OK = 20 + """Operation completed successfully.""" + REQ_REJECTED = 40 + """Request was rejected.""" + CREDENTIALS_REJECTED = 43 + """Credentials were rejected by the server.""" + UNKNOWN = 50 + """Unknown error occurred.""" + AUTH_UNREACHABLE = 53 + """Authentication server is unreachable.""" + BAD_VERSION = 55 + """Protocol version mismatch.""" + CLIENT_ID_TAKEN = 61 + """Client ID is already in use.""" + TX_VIOLATED_UNIQUE = 71 + """Transaction violated a unique constraint.""" class SyncChange: + """Sync incoming data event.""" + def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list[int]): + """Creates a SyncChange event. + + Args: + entity_id: Entity ID this change relates to. + entity: Entity type this change relates to. + puts: List of "put" (inserted/updated) object IDs. + removals: List of removed object IDs. + """ self.entity_id = entity_id + """Entity ID this change relates to.""" + self.entity = entity + """Entity type this change relates to.""" + self.puts = puts + """List of "put" (inserted/updated) object IDs.""" + self.removals = removals + """List of removed object IDs.""" class SyncLoginListener: + """Listener for sync login events. + + Implement this class and pass to SyncClient.set_login_listener() to receive + notifications about login success or failure. + """ def on_logged_in(self): + """Called when the client has successfully logged in to the server.""" pass def on_login_failed(self, sync_login_code: SyncCode): + """Called when login has failed. + + Args: + sync_login_code: The error code indicating why login failed. + """ pass class SyncConnectionListener: + """Listener for sync connection events. + + Implement this class and pass to SyncClient.set_connection_listener() to receive + notifications about connection state changes. + """ def on_connected(self): + """Called when the connection to the server is established.""" pass def on_disconnected(self): + """Called when the connection to the server is lost.""" pass class SyncErrorListener: + """Listener for sync error events. + + Implement this class and pass to SyncClient.set_error_listener() to receive + notifications about sync errors. + """ def on_error(self, sync_error_code: int): + """Called when a sync error occurs. + + Args: + sync_error_code: The error code indicating what error occurred. + """ pass class SyncClient: + """Sync client is used to connect to an ObjectBox sync server. + + Use through the Sync class factory methods. + """ def __init__(self, store: Store, server_urls: list[str], filter_variables: dict[str, str] | None = None): + """Creates a Sync client associated with the given store and options. + + This does not initiate any connection attempts yet: call start() to do so. + + Args: + store: The ObjectBox store to sync. + server_urls: List of server URLs to connect to. + filter_variables: Optional dictionary of filter variable names to values. + """ self.__c_login_listener = None self.__c_login_failure_listener = None self.__c_connect_listener = None @@ -171,6 +372,11 @@ def __check_sync_ptr_not_null(self): raise ValueError('SyncClient already closed') def set_credentials(self, credentials: SyncCredentials): + """Configure authentication credentials, depending on your server config. + + Args: + credentials: The credentials to use for authentication. + """ self.__check_sync_ptr_not_null() self.__credentials = credentials if isinstance(credentials, SyncCredentialsNone): @@ -186,6 +392,16 @@ def set_credentials(self, credentials: SyncCredentials): len(credentials.secret)) def set_multiple_credentials(self, credentials_list: list[SyncCredentials]): + """Like set_credentials, but accepts multiple credentials. + + However, does **not** support SyncCredentials.none(). + + Args: + credentials_list: List of credentials to use for authentication. + + Raises: + ValueError: If credentials_list is empty or contains SyncCredentials.none(). + """ self.__check_sync_ptr_not_null() if len(credentials_list) == 0: raise ValueError("Provide at least one credential") @@ -213,6 +429,13 @@ def set_multiple_credentials(self, credentials_list: list[SyncCredentials]): def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): + """Configures how sync updates are received from the server. + + If automatic updates are turned off, they will need to be requested manually. + + Args: + mode: The request updates mode to use. + """ self.__check_sync_ptr_not_null() if mode == SyncRequestUpdatesMode.MANUAL: c_mode = c.RequestUpdatesMode.MANUAL @@ -225,6 +448,11 @@ def set_request_updates_mode(self, mode: SyncRequestUpdatesMode): c.obx_sync_request_updates_mode(self.__c_sync_client_ptr, c_mode) def get_sync_state(self) -> SyncState: + """Gets the current sync client state. + + Returns: + The current SyncState of this client. + """ self.__check_sync_ptr_not_null() c_state = c.obx_sync_state(self.__c_sync_client_ptr) if c_state == c.SyncState.CREATED: @@ -245,40 +473,93 @@ def get_sync_state(self) -> SyncState: return SyncState.UNKNOWN def start(self): + """Once the sync client is configured, you can start it to initiate synchronization. + + This method triggers communication in the background and returns immediately. + The background thread will try to connect to the server, log-in and start + syncing data (depends on SyncRequestUpdatesMode). If the device, network or + server is currently offline, connection attempts will be retried later + automatically. If you haven't set the credentials in the options during + construction, call set_credentials() before start(). + """ self.__check_sync_ptr_not_null() c.obx_sync_start(self.__c_sync_client_ptr) def stop(self): + """Stops this sync client. Does nothing if it is already stopped.""" self.__check_sync_ptr_not_null() c.obx_sync_stop(self.__c_sync_client_ptr) def trigger_reconnect(self) -> bool: + """Triggers a reconnection attempt immediately. + + By default, an increasing backoff interval is used for reconnection attempts. + But sometimes the code using this API has additional knowledge and can + initiate a reconnection attempt sooner. + + Returns: + True if a reconnect was actually triggered. + """ self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_trigger_reconnect(self.__c_sync_client_ptr)) def request_updates(self, subscribe_for_future_pushes: bool) -> bool: + """Request updates since we last synchronized our database. + + Additionally, you can subscribe for future pushes from the server, to let + it send us future updates as they come in. + Call cancel_updates() to stop the updates. + + Args: + subscribe_for_future_pushes: If True, also subscribe for future pushes. + + Returns: + True if the request was successful. + """ self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_updates_request(self.__c_sync_client_ptr, subscribe_for_future_pushes)) def cancel_updates(self) -> bool: + """Cancel updates from the server so that it will stop sending updates. + + See also request_updates(). + + Returns: + True if the cancellation was successful. + """ self.__check_sync_ptr_not_null() return c.check_obx_success(c.obx_sync_updates_cancel(self.__c_sync_client_ptr)) @staticmethod def protocol_version() -> int: + """Returns the protocol version this client uses.""" return c.obx_sync_protocol_version() def protocol_server_version(self) -> int: + """Returns the protocol version of the server after a connection is + established (or attempted), zero otherwise. + """ return c.obx_sync_protocol_version_server(self.__c_sync_client_ptr) def close(self): + """Closes and cleans up all resources used by this sync client. + + It can no longer be used afterwards, make a new sync client instead. + Does nothing if this sync client has already been closed. + """ c.obx_sync_close(self.__c_sync_client_ptr) self.__c_sync_client_ptr = None def is_closed(self) -> bool: + """Returns if this sync client is closed and can no longer be used.""" return self.__c_sync_client_ptr is None def set_login_listener(self, login_listener: SyncLoginListener): + """Sets a listener to observe login events (success/failure). + + Args: + login_listener: The listener to receive login events. + """ self.__check_sync_ptr_not_null() self.__c_login_listener = c.OBX_sync_listener_login(lambda arg: login_listener.on_logged_in()) self.__c_login_failure_listener = c.OBX_sync_listener_login_failure( @@ -295,6 +576,11 @@ def set_login_listener(self, login_listener: SyncLoginListener): ) def set_connection_listener(self, connection_listener: SyncConnectionListener): + """Sets a listener to observe connection state changes (connect/disconnect). + + Args: + connection_listener: The listener to receive connection events. + """ self.__check_sync_ptr_not_null() self.__c_connect_listener = c.OBX_sync_listener_connect(lambda arg: connection_listener.on_connected()) self.__c_disconnect_listener = c.OBX_sync_listener_disconnect(lambda arg: connection_listener.on_disconnected()) @@ -310,6 +596,11 @@ def set_connection_listener(self, connection_listener: SyncConnectionListener): ) def set_error_listener(self, error_listener: SyncErrorListener): + """Sets a listener to observe sync error events. + + Args: + error_listener: The listener to receive error events. + """ self.__check_sync_ptr_not_null() self.__c_error_listener = c.OBX_sync_listener_error( lambda arg, sync_error_code: error_listener.on_error(sync_error_code)) @@ -320,22 +611,70 @@ def set_error_listener(self, error_listener: SyncErrorListener): ) def wait_for_logged_in_state(self, timeout_millis: int): + """Waits for the sync client to reach the logged-in state. + + Args: + timeout_millis: Maximum time to wait in milliseconds. + """ self.__check_sync_ptr_not_null() c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis) def add_filter_variable(self, name: str, value: str): + """Adds or replaces a Sync filter variable value for the given name. + + Eventually, existing values for the same name are replaced. + + Sync client filter variables can be used in server-side Sync filters to + filter out objects that do not match the filters. Filter variables must be + added before login, so before calling start(). + + See also remove_filter_variable() and remove_all_filter_variables(). + + Args: + name: The name of the filter variable. + value: The value of the filter variable. + """ self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_put(self.__c_sync_client_ptr, name.encode('utf-8'), value.encode('utf-8')) def remove_filter_variable(self, name: str): + """Removes a previously added Sync filter variable value. + + See also add_filter_variable() and remove_all_filter_variables(). + + Args: + name: The name of the filter variable to remove. + """ self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_remove(self.__c_sync_client_ptr, name.encode('utf-8')) def remove_all_filter_variables(self): + """Removes all previously added Sync filter variable values. + + See also add_filter_variable() and remove_filter_variable(). + """ self.__check_sync_ptr_not_null() c.obx_sync_filter_variables_remove_all(self.__c_sync_client_ptr) def get_outgoing_message_count(self, limit: int = 0) -> int: + """Count the number of messages in the outgoing queue, i.e. those waiting + to be sent to the server. + + By default, counts all messages without any limitation. For a lower number + pass a limit that's enough for your app logic. + + Note: This call uses a (read) transaction internally: + 1) It's not just a "cheap" return of a single number. While this will + still be fast, avoid calling this function excessively. + 2) The result follows transaction view semantics, thus it may not always + match the actual value. + + Args: + limit: Optional limit for counting messages. Default is 0 (no limit). + + Returns: + The number of messages in the outgoing queue. + """ self.__check_sync_ptr_not_null() outgoing_message_count = ctypes.c_uint64(0) c.obx_sync_outgoing_message_count(self.__c_sync_client_ptr, limit, ctypes.byref(outgoing_message_count)) @@ -343,10 +682,16 @@ def get_outgoing_message_count(self, limit: int = 0) -> int: class Sync: + """ObjectBox Sync makes data available and synchronized across devices, + online and offline. + + Start a client using Sync.client() and connect to a remote server. + """ __sync_clients: dict[Store, SyncClient] = {} @staticmethod def is_available() -> bool: + """Returns True if the loaded ObjectBox native library supports Sync.""" return c.obx_has_feature(c.Feature.Sync) @staticmethod @@ -356,6 +701,28 @@ def client( credential: SyncCredentials, filter_variables: dict[str, str] | None = None ) -> SyncClient: + """Creates a Sync client associated with the given store and configures it + with the given options. + + This does not initiate any connection attempts yet, call SyncClient.start() + to do so. + + Before SyncClient.start(), you can still configure some aspects of the + client, e.g. its request updates mode. + + To configure Sync filter variables, pass variable names mapped to their + value to filter_variables. Sync client filter variables can be used in + server-side Sync filters to filter out objects that do not match the filter. + + Args: + store: The ObjectBox store to sync. + server_url: The URL of the sync server to connect to. + credential: The credentials to use for authentication. + filter_variables: Optional dictionary of filter variable names to values. + + Returns: + A configured SyncClient instance. + """ client = SyncClient(store, [server_url], filter_variables) client.set_credentials(credential) return client @@ -367,6 +734,20 @@ def client_multi_creds( credentials_list: list[SyncCredentials], filter_variables: dict[str, str] | None = None ) -> SyncClient: + """Like client(), but accepts a list of credentials. + + When passing multiple credentials, does **not** support + SyncCredentials.none(). + + Args: + store: The ObjectBox store to sync. + server_url: The URL of the sync server to connect to. + credentials_list: List of credentials to use for authentication. + filter_variables: Optional dictionary of filter variable names to values. + + Returns: + A configured SyncClient instance. + """ client = SyncClient(store, [server_url], filter_variables) client.set_multiple_credentials(credentials_list) return client @@ -378,6 +759,17 @@ def client_multi_urls( credential: SyncCredentials, filter_variables: dict[str, str] | None = None ) -> SyncClient: + """Like client(), but accepts a list of URLs to work with multiple servers. + + Args: + store: The ObjectBox store to sync. + server_urls: List of server URLs to connect to. + credential: The credentials to use for authentication. + filter_variables: Optional dictionary of filter variable names to values. + + Returns: + A configured SyncClient instance. + """ client = SyncClient(store, server_urls, filter_variables) client.set_credentials(credential) return client @@ -389,6 +781,24 @@ def client_multi_creds_multi_urls( credentials_list: list[SyncCredentials], filter_variables: dict[str, str] | None = None ) -> SyncClient: + """Like client(), but accepts a list of credentials and a list of URLs to + work with multiple servers. + + When passing multiple credentials, does **not** support + SyncCredentials.none(). + + Args: + store: The ObjectBox store to sync. + server_urls: List of server URLs to connect to. + credentials_list: List of credentials to use for authentication. + filter_variables: Optional dictionary of filter variable names to values. + + Returns: + A configured SyncClient instance. + + Raises: + ValueError: If a sync client is already active for the given store. + """ if store in Sync.__sync_clients: raise ValueError('Only one sync client can be active for a store') client = SyncClient(store, server_urls, filter_variables) From 37bee34205b19c8acacc60955d594e44f3136f4e Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 1 Jan 2026 11:09:03 +0530 Subject: [PATCH 21/21] Add change listener to notify client on incoming changes --- objectbox/c.py | 4 +++ objectbox/sync.py | 64 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 141ca06..9a8d8b4 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -1273,6 +1273,10 @@ class OBX_sync_msg_objects_builder(ctypes.Structure): # void obx_sync_listener_complete(OBX_sync* sync, OBX_sync_listener_complete* listener, void* listener_arg); obx_sync_listener_error = c_fn('obx_sync_listener_error', None, [OBX_sync_p, OBX_sync_listener_error, ctypes.c_void_p]) +# void obx_sync_listener_change(OBX_sync* sync, OBX_sync_listener_change* listener, void* listener_arg); +obx_sync_listener_change = c_fn('obx_sync_listener_change', None, + [OBX_sync_p, OBX_sync_listener_change, ctypes.c_void_p]) + # Filter Variables # obx_err obx_sync_filter_variables_put(OBX_sync* sync, const char* name, const char* value); diff --git a/objectbox/sync.py b/objectbox/sync.py index c176982..1549334 100644 --- a/objectbox/sync.py +++ b/objectbox/sync.py @@ -3,6 +3,8 @@ import objectbox.c as c from objectbox import Store +from objectbox.c import OBX_sync_change_array + class SyncCredentials: """Credentials used to authenticate a sync client against a server.""" @@ -245,21 +247,17 @@ class SyncCode(IntEnum): class SyncChange: """Sync incoming data event.""" - def __init__(self, entity_id: int, entity: type, puts: list[int], removals: list[int]): + def __init__(self, entity_id: int, puts: list[int], removals: list[int]): """Creates a SyncChange event. Args: entity_id: Entity ID this change relates to. - entity: Entity type this change relates to. puts: List of "put" (inserted/updated) object IDs. removals: List of removed object IDs. """ self.entity_id = entity_id """Entity ID this change relates to.""" - self.entity = entity - """Entity type this change relates to.""" - self.puts = puts """List of "put" (inserted/updated) object IDs.""" @@ -319,6 +317,17 @@ def on_error(self, sync_error_code: int): pass +class SyncChangeListener: + + def on_change(self, sync_changes: list[SyncChange]): + """Called when incoming data changes are received from the server. + + Args: + sync_changes: List of SyncChange events representing the changes. + """ + pass + + class SyncClient: """Sync client is used to connect to an ObjectBox sync server. @@ -336,6 +345,7 @@ def __init__(self, store: Store, server_urls: list[str], server_urls: List of server URLs to connect to. filter_variables: Optional dictionary of filter variable names to values. """ + self.__c_change_listener = None self.__c_login_listener = None self.__c_login_failure_listener = None self.__c_connect_listener = None @@ -547,6 +557,12 @@ def close(self): It can no longer be used afterwards, make a new sync client instead. Does nothing if this sync client has already been closed. """ + c.obx_sync_listener_error(self.__c_sync_client_ptr, None, None) + c.obx_sync_listener_login(self.__c_sync_client_ptr, None, None) + c.obx_sync_listener_login_failure(self.__c_sync_client_ptr, None, None) + c.obx_sync_listener_connect(self.__c_sync_client_ptr, None, None) + c.obx_sync_listener_disconnect(self.__c_sync_client_ptr, None, None) + c.obx_sync_listener_change(self.__c_sync_client_ptr, None, None) c.obx_sync_close(self.__c_sync_client_ptr) self.__c_sync_client_ptr = None @@ -610,6 +626,44 @@ def set_error_listener(self, error_listener: SyncErrorListener): None ) + def set_change_listener(self, change_listener: SyncChangeListener): + """Sets a listener to observe incoming data changes from the server. + + Args: + change_listener: The listener to receive change events. + """ + self.__check_sync_ptr_not_null() + + def c_change_callback(arg, sync_change_array_ptr): + sync_change_array = ctypes.cast(sync_change_array_ptr, ctypes.POINTER(OBX_sync_change_array)).contents + changes: list[SyncChange] = [] + for i in range(sync_change_array.count): + c_sync_change: c.OBX_sync_change = sync_change_array.list[i] + puts = [] + if c_sync_change.puts: + c_puts_id_array: c.OBX_id_array = ctypes.cast(c_sync_change.puts, c.OBX_id_array_p).contents + puts = list( + ctypes.cast(c_puts_id_array.ids, ctypes.POINTER(c.obx_id * c_puts_id_array.count)).contents) + removals = [] + if c_sync_change.removals: + c_removals_id_array: c.OBX_id_array = ctypes.cast(c_sync_change.removals, c.OBX_id_array_p).contents + removals = list( + ctypes.cast(c_removals_id_array.ids, + ctypes.POINTER(c.obx_id * c_removals_id_array.count)).contents) + changes.append(SyncChange( + entity_id=c_sync_change.entity_id, + puts=puts, + removals=removals + )) + change_listener.on_change(changes) + + self.__c_change_listener = c.OBX_sync_listener_change(c_change_callback) + c.obx_sync_listener_change( + self.__c_sync_client_ptr, + self.__c_change_listener, + None + ) + def wait_for_logged_in_state(self, timeout_millis: int): """Waits for the sync client to reach the logged-in state.