objectbox-api 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
objectbox/sync.py ADDED
@@ -0,0 +1,1101 @@
1
+ import ctypes
2
+ import threading
3
+ import typing
4
+ from enum import Enum, auto, IntEnum, IntFlag
5
+
6
+ import objectbox.c as c
7
+ from objectbox import Store
8
+ from objectbox.model.entity import _Entity
9
+ from objectbox.c import OBX_sync_change_array
10
+ from objectbox.exceptions import FeatureNotAvailableError, IllegalArgumentError, IllegalStateError
11
+
12
+ __all__ = [
13
+ 'Sync',
14
+ 'SyncClient',
15
+ 'SyncCredentials',
16
+ 'SyncState',
17
+ 'SyncRequestUpdatesMode',
18
+ 'SyncFlags',
19
+ 'SyncStats',
20
+ 'SyncCode',
21
+ 'SyncChange',
22
+ 'SyncListener',
23
+ 'SyncLoginListener',
24
+ 'SyncConnectionListener',
25
+ 'SyncCompletedListener',
26
+ 'SyncTimeListener',
27
+ 'SyncChangeListener',
28
+ ]
29
+
30
+
31
+ def _to_bytearray(value: typing.Union[str, bytes]) -> bytearray:
32
+ """Returns a new bytearray (UTF-8 for a str), which can be wiped after use (unlike str and bytes)."""
33
+ if isinstance(value, str):
34
+ return bytearray(value, 'utf-8')
35
+ if isinstance(value, (bytes, bytearray)):
36
+ return bytearray(value)
37
+ raise TypeError(f"Expected str or bytes, got {type(value).__name__}")
38
+
39
+
40
+ class SyncCredentials:
41
+ """Credentials used to authenticate a sync client against a server."""
42
+
43
+ def __init__(self, credential_type: c.SyncCredentialsType):
44
+ self.type = credential_type
45
+
46
+ @staticmethod
47
+ def none() -> 'SyncCredentials':
48
+ """No credentials - usually only for development purposes with a server
49
+ configured to accept all connections without authentication.
50
+
51
+ Returns:
52
+ A SyncCredentials instance with no authentication.
53
+ """
54
+ return SyncCredentialsNone()
55
+
56
+ @staticmethod
57
+ def shared_secret(secret: typing.Union[str, bytes]) -> 'SyncCredentials':
58
+ """Shared secret authentication.
59
+
60
+ Args:
61
+ secret: The shared secret, a string or bytes.
62
+
63
+ Returns:
64
+ A SyncCredentials instance for shared secret authentication.
65
+ """
66
+ return SyncCredentialsSecret(c.SyncCredentialsType.SHARED_SECRET_SIPPED, _to_bytearray(secret))
67
+
68
+ @staticmethod
69
+ def google_auth(token: typing.Union[str, bytes]) -> 'SyncCredentials':
70
+ """Google authentication.
71
+
72
+ Args:
73
+ token: The Google ID token, a string or bytes.
74
+
75
+ Returns:
76
+ A SyncCredentials instance for Google authentication.
77
+ """
78
+ return SyncCredentialsSecret(c.SyncCredentialsType.GOOGLE_AUTH, _to_bytearray(token))
79
+
80
+ @staticmethod
81
+ def user_and_password(username: str, password: str) -> 'SyncCredentials':
82
+ """Username and password authentication.
83
+
84
+ Args:
85
+ username: The username.
86
+ password: The password.
87
+
88
+ Returns:
89
+ A SyncCredentials instance for username/password authentication.
90
+ """
91
+ return SyncCredentialsUserPassword(c.SyncCredentialsType.USER_PASSWORD, username, password)
92
+
93
+ @staticmethod
94
+ def obx_admin_user(username: str, password: str) -> 'SyncCredentials':
95
+ """ObjectBox Admin user (username and password) authentication.
96
+
97
+ Args:
98
+ username: The username.
99
+ password: The password.
100
+
101
+ Returns:
102
+ A SyncCredentials instance for ObjectBox Admin user authentication.
103
+ """
104
+ return SyncCredentialsUserPassword(c.SyncCredentialsType.OBX_ADMIN_USER, username, password)
105
+
106
+ @staticmethod
107
+ def jwt_id_token(jwt_id_token: str) -> 'SyncCredentials':
108
+ """JSON Web Token (JWT): an ID token that typically provides identity
109
+ information about the authenticated user.
110
+
111
+ Args:
112
+ jwt_id_token: The JWT ID token.
113
+
114
+ Returns:
115
+ A SyncCredentials instance for JWT ID token authentication.
116
+ """
117
+ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ID, _to_bytearray(jwt_id_token))
118
+
119
+ @staticmethod
120
+ def jwt_access_token(jwt_access_token: str) -> 'SyncCredentials':
121
+ """JSON Web Token (JWT): an access token that is used to access resources.
122
+
123
+ Args:
124
+ jwt_access_token: The JWT access token.
125
+
126
+ Returns:
127
+ A SyncCredentials instance for JWT access token authentication.
128
+ """
129
+ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_ACCESS, _to_bytearray(jwt_access_token))
130
+
131
+ @staticmethod
132
+ def jwt_refresh_token(jwt_refresh_token: str) -> 'SyncCredentials':
133
+ """JSON Web Token (JWT): a refresh token that is used to obtain a new
134
+ access token.
135
+
136
+ Args:
137
+ jwt_refresh_token: The JWT refresh token.
138
+
139
+ Returns:
140
+ A SyncCredentials instance for JWT refresh token authentication.
141
+ """
142
+ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_REFRESH, _to_bytearray(jwt_refresh_token))
143
+
144
+ @staticmethod
145
+ def jwt_custom_token(jwt_custom_token: str) -> 'SyncCredentials':
146
+ """JSON Web Token (JWT): a token that is neither an ID, access,
147
+ nor refresh token.
148
+
149
+ Args:
150
+ jwt_custom_token: The custom JWT token.
151
+
152
+ Returns:
153
+ A SyncCredentials instance for custom JWT token authentication.
154
+ """
155
+ return SyncCredentialsSecret(c.SyncCredentialsType.JWT_CUSTOM, _to_bytearray(jwt_custom_token))
156
+
157
+
158
+ class SyncCredentialsNone(SyncCredentials):
159
+ """Internal use only. Represents no credentials for authentication."""
160
+
161
+ def __init__(self):
162
+ super().__init__(c.SyncCredentialsType.NONE)
163
+
164
+
165
+ class SyncCredentialsSecret(SyncCredentials):
166
+ """Internal use only. Sync credential that is a single secret string.
167
+
168
+ Like in Java, the secret is wiped (overwritten with zeros) once it was passed to the C library, so these
169
+ credentials can only be used once.
170
+ """
171
+
172
+ def __init__(self, credential_type: c.SyncCredentialsType, secret: bytearray):
173
+ """Creates a secret-based credential.
174
+
175
+ Args:
176
+ credential_type: The type of credential.
177
+ secret: UTF-8 encoded secret bytes; must not be empty. Wiped after use, so pass a copy.
178
+ """
179
+ if len(secret) == 0:
180
+ raise IllegalArgumentError("Token must not be empty")
181
+ super().__init__(credential_type)
182
+ self.secret = secret
183
+ self.cleared = False
184
+
185
+ def _check_not_cleared(self):
186
+ if self.cleared:
187
+ raise IllegalStateError("Credentials already have been used (and cleared); create new credentials")
188
+
189
+ def _pass_secret(self, c_function: typing.Callable[[ctypes.Array, int], None]):
190
+ """Calls c_function(buffer, size) with the secret and wipes the secret afterwards (also if it raises)."""
191
+ self._check_not_cleared()
192
+ try:
193
+ buffer = (ctypes.c_char * len(self.secret)).from_buffer(self.secret)
194
+ try:
195
+ c_function(buffer, len(self.secret))
196
+ finally:
197
+ del buffer # Release the bytearray's buffer
198
+ finally:
199
+ self.secret[:] = bytes(len(self.secret))
200
+ self.cleared = True
201
+
202
+
203
+ class SyncCredentialsUserPassword(SyncCredentials):
204
+ """Internal use only. Sync credential with username and password."""
205
+
206
+ def __init__(self, credential_type: c.SyncCredentialsType, username: str, password: str):
207
+ """Creates a username/password credential.
208
+
209
+ Args:
210
+ credential_type: The type of credential.
211
+ username: The username.
212
+ password: The password.
213
+ """
214
+ super().__init__(credential_type)
215
+ self.username = username
216
+ self.password = password
217
+
218
+
219
+ class SyncState(Enum):
220
+ """Current state of the SyncClient."""
221
+
222
+ UNKNOWN = auto()
223
+ """State is unknown, e.g. C-API reported a state that's not recognized yet."""
224
+
225
+ CREATED = auto()
226
+ """Client created but not yet started."""
227
+
228
+ STARTED = auto()
229
+ """Client started and connecting."""
230
+
231
+ CONNECTED = auto()
232
+ """Connection with the server established but not authenticated yet."""
233
+
234
+ LOGGED_IN = auto()
235
+ """Client authenticated and synchronizing."""
236
+
237
+ DISCONNECTED = auto()
238
+ """Lost connection, will try to reconnect if the credentials are valid."""
239
+
240
+ STOPPED = auto()
241
+ """Client in the process of being closed."""
242
+
243
+ DEAD = auto()
244
+ """Invalid access to the client after it was closed."""
245
+
246
+
247
+ class SyncRequestUpdatesMode(IntEnum):
248
+ """Configuration of how SyncClient fetches remote updates from the server."""
249
+
250
+ MANUAL = c.RequestUpdatesMode.MANUAL
251
+ """No updates, SyncClient.request_updates() must be called manually."""
252
+
253
+ AUTO = c.RequestUpdatesMode.AUTO
254
+ """Automatic updates, including subsequent pushes from the server, same as
255
+ calling SyncClient.request_updates(True). This is the default unless
256
+ changed by SyncClient.set_request_updates_mode()."""
257
+
258
+ AUTO_NO_PUSHES = c.RequestUpdatesMode.AUTO_NO_PUSHES
259
+ """Automatic update after connection, without subscribing for pushes from the
260
+ server. Similar to calling SyncClient.request_updates(False)."""
261
+
262
+
263
+ class SyncFlags(IntFlag):
264
+ """Flags to adjust the Sync client behavior (see Sync.client()); combine them with ``|``."""
265
+
266
+ DEBUG_LOG_ID_MAPPING = 1
267
+ """Enables (rather extensive) logging on how IDs are mapped (local <-> global)."""
268
+
269
+ KEEP_DATA_ON_SYNC_ERROR = 2
270
+ """If the client gets in a state that does not allow any further synchronization, this flag instructs Sync to keep
271
+ local data nevertheless. While this preserves data, you need to resolve the situation manually, e.g. back up the
272
+ data and start with a fresh database. By default (without this flag), existing data of all Sync entity types is
273
+ wiped and synced from scratch from the server."""
274
+
275
+ DEBUG_LOG_FILTER_VARIABLES = 4
276
+ """Logs Sync filter variables used for each client (server-only: no effect for Sync clients)."""
277
+
278
+ REMOVE_WITH_OBJECT_DATA = 8
279
+ """Remove operations include the full object data in the TX log. This allows Sync filters to filter out remove
280
+ operations based on the object content; without this flag, remove operations only contain the object ID and
281
+ cannot be filtered. Note: this increases the size of TX logs for remove operations."""
282
+
283
+ DEBUG_LOG_TX_LOGS = 16
284
+ """Enables debug logging of TX log processing."""
285
+
286
+ SKIP_INVALID_TX_OPS = 32
287
+ """Skips invalid (put object) operations in the TX log instead of failing."""
288
+
289
+
290
+ class SyncStats(IntEnum):
291
+ """Sync client statistics counters, useful for testing and diagnostics; see SyncClient.stats()."""
292
+
293
+ CONNECTS = 1
294
+ """Total number of connects."""
295
+
296
+ LOGINS = 2
297
+ """Total number of successful logins."""
298
+
299
+ MESSAGES_RECEIVED = 3
300
+ """Total number of messages received."""
301
+
302
+ MESSAGES_SENT = 4
303
+ """Total number of messages sent."""
304
+
305
+ MESSAGE_SEND_FAILURES = 5
306
+ """Total number of errors during message sending."""
307
+
308
+ MESSAGE_BYTES_RECEIVED = 6
309
+ """Total number of bytes received via messages. Note: this is measured on the application level and thus may not
310
+ match e.g. the network level."""
311
+
312
+ MESSAGE_BYTES_SENT = 7
313
+ """Total number of bytes sent via messages. Note: this is measured on the application level and thus may not
314
+ match e.g. the network level."""
315
+
316
+
317
+ class SyncCode(IntEnum):
318
+ """Sync response/error codes."""
319
+
320
+ OK = 20
321
+ """Operation completed successfully."""
322
+
323
+ REQ_REJECTED = 40
324
+ """Request was rejected."""
325
+
326
+ CREDENTIALS_REJECTED = 43
327
+ """Credentials were rejected by the server."""
328
+
329
+ UNKNOWN = 50
330
+ """Unknown error occurred."""
331
+
332
+ AUTH_UNREACHABLE = 53
333
+ """Authentication server is unreachable."""
334
+
335
+ BAD_VERSION = 55
336
+ """Protocol version mismatch."""
337
+
338
+ CLIENT_ID_TAKEN = 61
339
+ """Client ID is already in use."""
340
+
341
+ TX_VIOLATED_UNIQUE = 71
342
+ """Transaction violated a unique constraint."""
343
+
344
+
345
+ def _to_enum(enum_type, value):
346
+ """Converts to the given enum; raises IllegalArgumentError (a ValueError) for other values."""
347
+ try:
348
+ return enum_type(value)
349
+ except ValueError:
350
+ raise IllegalArgumentError(f"Not a {enum_type.__name__} value: {value!r}") from None
351
+
352
+
353
+ def _to_sync_code(code: int) -> typing.Union[SyncCode, int]:
354
+ """Converts to SyncCode; unknown codes (e.g. added by a newer library) are passed as int."""
355
+ try:
356
+ return SyncCode(code)
357
+ except ValueError:
358
+ return code
359
+
360
+
361
+ class SyncChange:
362
+ """Sync incoming data event: the changes received from the Sync server for a single entity type."""
363
+
364
+ def __init__(self, entity_id: int, puts: typing.List[int], removals: typing.List[int],
365
+ entity: typing.Optional[_Entity] = None):
366
+ """Creates a SyncChange event.
367
+
368
+ Args:
369
+ entity_id: Entity ID this change relates to.
370
+ puts: List of "put" (inserted/updated) object IDs.
371
+ removals: List of removed object IDs.
372
+ entity: The entity (class) this change relates to, if known.
373
+ """
374
+ self.entity_id = entity_id
375
+ """Entity ID this change relates to."""
376
+
377
+ self.entity = entity
378
+ """The entity (class decorated with @Entity()) this change relates to, e.g. to compare with or for
379
+ store.box(change.entity); None if the store was opened without a model."""
380
+
381
+ self.puts = puts
382
+ """List of "put" (inserted/updated) object IDs."""
383
+
384
+ self.removals = removals
385
+ """List of removed object IDs."""
386
+
387
+
388
+ class SyncLoginListener:
389
+ """Listener for sync login events.
390
+
391
+ Implement this class and pass to SyncClient.set_login_listener() to receive
392
+ notifications about login success or failure.
393
+
394
+ Note: like all Sync listeners, it is called from the sync client's background thread; it should return quickly
395
+ and must not close the client.
396
+ """
397
+
398
+ def on_logged_in(self):
399
+ """Called when the client has successfully logged in to the server."""
400
+ pass
401
+
402
+ def on_login_failed(self, sync_login_code: SyncCode):
403
+ """Called when login has failed.
404
+
405
+ Args:
406
+ sync_login_code: The error code indicating why login failed (an int if the code is unknown).
407
+ """
408
+ pass
409
+
410
+
411
+ class SyncConnectionListener:
412
+ """Listener for sync connection events.
413
+
414
+ Implement this class and pass to SyncClient.set_connection_listener() to receive
415
+ notifications about connection state changes.
416
+ """
417
+
418
+ def on_connected(self):
419
+ """Called when the connection to the server is established."""
420
+ pass
421
+
422
+ def on_disconnected(self):
423
+ """Called when the connection to the server is lost."""
424
+ pass
425
+
426
+
427
+ class SyncCompletedListener:
428
+ """Listener for sync completed events.
429
+
430
+ Implement this class and pass to SyncClient.set_completed_listener() to be notified when synchronization has
431
+ completed. Note: called from the sync client's background thread (see SyncLoginListener).
432
+ """
433
+
434
+ def on_updates_completed(self):
435
+ """Called each time a sync has completed, i.e. the client caught up with the current server state
436
+ (the client is "up-to-date")."""
437
+ pass
438
+
439
+
440
+ class SyncTimeListener:
441
+ """Listener for server time updates.
442
+
443
+ Implement this class and pass to SyncClient.set_time_listener(). Note: called from the sync client's background
444
+ thread (see SyncLoginListener).
445
+ """
446
+
447
+ def on_server_time_update(self, server_time_nanos: int):
448
+ """Called when server time information is received on the client.
449
+
450
+ Args:
451
+ server_time_nanos: The server time as a Unix timestamp in nanoseconds (since epoch).
452
+ """
453
+ pass
454
+
455
+
456
+ class SyncChangeListener:
457
+ """Listener for incoming data changes from the server.
458
+
459
+ Implement this class and pass to SyncClient.set_change_listener(). Note: called from the sync client's background
460
+ thread (see SyncLoginListener).
461
+ """
462
+
463
+ def on_change(self, sync_changes: typing.List[SyncChange]):
464
+ """Called when incoming data changes are received from the server.
465
+
466
+ Args:
467
+ sync_changes: List of SyncChange events representing the changes.
468
+ """
469
+ pass
470
+
471
+
472
+ class SyncListener(SyncLoginListener, SyncCompletedListener, SyncConnectionListener, SyncChangeListener,
473
+ SyncTimeListener):
474
+ """Listener for all Sync events (login, completed, connection, change and server time); set via
475
+ SyncClient.set_listener(). Override the methods for the events of interest (the others do nothing).
476
+
477
+ Use the more specific listeners, like SyncLoginListener, to only receive a sub-set of events.
478
+ """
479
+ pass
480
+
481
+
482
+ class SyncClient:
483
+ """Sync client is used to connect to an ObjectBox sync server.
484
+
485
+ Create it via Sync.client() (the constructor is internal). A store has at most one open client, which it keeps
486
+ alive (no need to keep a reference to it) until the client or the store is closed; see Store.sync_client().
487
+ """
488
+
489
+ def __init__(self, *args, **kwargs):
490
+ raise TypeError("Use Sync.client() to create a SyncClient")
491
+
492
+ @classmethod
493
+ def _create(cls, store: Store, urls: typing.List[str],
494
+ credentials: typing.Union[SyncCredentials, typing.List[SyncCredentials]],
495
+ filter_variables: typing.Optional[typing.Dict[str, str]], flags: int,
496
+ certificate_paths: typing.Optional[typing.List[str]]) -> 'SyncClient':
497
+ """Internal: creates a client for Sync.client() (see there for the parameters)."""
498
+ client = cls.__new__(cls)
499
+ client.__init_client(store, urls, flags, certificate_paths)
500
+ try:
501
+ for name, value in (filter_variables or {}).items():
502
+ client.put_filter_variable(name, value)
503
+ client.set_credentials(credentials)
504
+ except BaseException:
505
+ client.close() # Not reachable by the caller, so close it now (it would block a new client for the store)
506
+ raise
507
+ return client
508
+
509
+ def __init_client(self, store: Store, urls: typing.List[str], flags: int,
510
+ certificate_paths: typing.Optional[typing.List[str]]):
511
+ # Set first, so close() (e.g. via __del__) works if this raises
512
+ self.__c_sync_client_ptr = None
513
+ self.__close_lock = threading.Lock()
514
+ self.__on_store_close = None
515
+ # All ctypes callbacks passed to C: the C client may call them until it is closed, also replaced ones
516
+ # (e.g. while a listener is replaced, the client's thread may be calling the previous one), so keep them all.
517
+ self.__c_callbacks = []
518
+ if not urls:
519
+ raise IllegalArgumentError("Provide at least one server URL")
520
+
521
+ if not Sync.is_available():
522
+ raise FeatureNotAvailableError(
523
+ 'Sync is not available in the loaded ObjectBox runtime library. '
524
+ 'Install ObjectBox with the Sync variant of the library via "pip install objectbox-sync" '
525
+ '(instead of objectbox); '
526
+ 'please visit https://objectbox.io/sync/ for options.')
527
+
528
+ self.__store = store
529
+
530
+ store._c_store # Raises if the store is closed
531
+ if store._sync_client is not None:
532
+ raise IllegalStateError("Only one sync client can be active for a store")
533
+
534
+ # Like Java and Dart, create the client via options (allows further options, e.g. flags)
535
+ c_options = c.obx_sync_opt(store._c_store)
536
+ try:
537
+ for url in urls:
538
+ c.obx_sync_opt_add_url(c_options, url.encode('utf-8'))
539
+ for certificate_path in certificate_paths or []:
540
+ c.obx_sync_opt_add_cert_path(c_options, certificate_path.encode('utf-8'))
541
+ if flags:
542
+ c.obx_sync_opt_flags(c_options, flags)
543
+ except BaseException:
544
+ c.obx_sync_opt_free(c_options) # Only if not passed to obx_sync_create(), which frees them in any case
545
+ raise
546
+ self.__c_sync_client_ptr = c.obx_sync_create(c_options)
547
+
548
+ # Close this client when the store is closed (the C API requires the client to be closed first).
549
+ # Like in Java and Dart, the store keeps the client alive until it is closed, so the caller does not have to
550
+ # keep a reference (a store and its client that are both no longer referenced are closed by the cyclic GC).
551
+ self.__on_store_close = self.close
552
+ self.__store._add_close_listener(self.__on_store_close)
553
+ store._sync_client = self
554
+
555
+ def __del__(self):
556
+ # Close the SyncClient when this instance is destructed, e.g. garbage collected together with its store.
557
+ # Not if it was never initialized (the constructor raises; use Sync.client()).
558
+ if getattr(self, '_SyncClient__c_sync_client_ptr', None) is not None:
559
+ self.close()
560
+
561
+ def __check_sync_ptr_not_null(self):
562
+ if self.__c_sync_client_ptr is None:
563
+ raise IllegalStateError('SyncClient already closed')
564
+
565
+ def set_credentials(self, credentials: typing.Union[SyncCredentials, typing.List[SyncCredentials]]):
566
+ """Configure authentication credentials, depending on your server config.
567
+
568
+ Any credentials set before are replaced. Usually, credentials are passed to Sync.client(), but this can be used
569
+ to update them later, e.g. when a token expires.
570
+
571
+ Like in Java, secrets (tokens) are wiped from the given credentials once passed on, so credentials can only be
572
+ used once; create new ones e.g. to set them again.
573
+
574
+ Args:
575
+ credentials: The credentials to use for authentication; a list to pass multiple credentials (the server
576
+ accepts the client if any of them is valid), which must not include SyncCredentials.none().
577
+
578
+ Raises:
579
+ IllegalArgumentError: If the list is empty or contains SyncCredentials.none() (along with other
580
+ credentials).
581
+ IllegalStateError: If credentials were already used.
582
+ TypeError: If credentials were not created by the SyncCredentials factory methods.
583
+ """
584
+ self.__check_sync_ptr_not_null()
585
+ credentials_list = list(credentials) if isinstance(credentials, (list, tuple)) else [credentials]
586
+ if len(credentials_list) == 0:
587
+ raise IllegalArgumentError("Provide at least one credential")
588
+ # Check all before adding any, as the C client would be left with an incomplete set of credentials
589
+ for creds in credentials_list:
590
+ if not isinstance(creds, (SyncCredentialsNone, SyncCredentialsUserPassword, SyncCredentialsSecret)):
591
+ raise TypeError(f"Unsupported credentials: {creds!r}; use the SyncCredentials factory methods")
592
+ if isinstance(creds, SyncCredentialsNone) and len(credentials_list) > 1:
593
+ raise IllegalArgumentError("SyncCredentials.none() is not supported with multiple credentials")
594
+ if isinstance(creds, SyncCredentialsSecret):
595
+ creds._check_not_cleared()
596
+
597
+ if len(credentials_list) == 1:
598
+ creds = credentials_list[0]
599
+ if isinstance(creds, SyncCredentialsNone):
600
+ c.obx_sync_credentials(self.__c_sync_client_ptr, creds.type, None, 0)
601
+ elif isinstance(creds, SyncCredentialsUserPassword):
602
+ c.obx_sync_credentials_user_password(self.__c_sync_client_ptr, creds.type,
603
+ creds.username.encode('utf-8'), creds.password.encode('utf-8'))
604
+ else:
605
+ creds._pass_secret(lambda buffer, size: c.obx_sync_credentials(self.__c_sync_client_ptr, creds.type,
606
+ buffer, size))
607
+ return
608
+
609
+ for i, creds in enumerate(credentials_list):
610
+ is_last = (i == len(credentials_list) - 1)
611
+ if isinstance(creds, SyncCredentialsUserPassword):
612
+ c.obx_sync_credentials_add_user_password(self.__c_sync_client_ptr, creds.type,
613
+ creds.username.encode('utf-8'),
614
+ creds.password.encode('utf-8'), is_last)
615
+ else:
616
+ creds._pass_secret(lambda buffer, size: c.obx_sync_credentials_add(self.__c_sync_client_ptr,
617
+ creds.type, buffer, size, is_last))
618
+
619
+ def stats(self, counter: SyncStats) -> int:
620
+ """Gets the value of a Sync client statistics counter.
621
+
622
+ Args:
623
+ counter: The counter to get, see SyncStats.
624
+
625
+ Raises:
626
+ IllegalArgumentError: If counter is not a SyncStats value.
627
+ """
628
+ self.__check_sync_ptr_not_null()
629
+ count = ctypes.c_uint64(0)
630
+ c.obx_sync_stats_u64(self.__c_sync_client_ptr, _to_enum(SyncStats, counter), ctypes.byref(count))
631
+ return count.value
632
+
633
+ def set_max_messages_in_flight(self, value: int):
634
+ """Configures the maximum number of outgoing transaction messages that can be sent without an ACK from the
635
+ server.
636
+
637
+ Args:
638
+ value: The maximum number of messages; must be in the range 1-20.
639
+
640
+ Raises:
641
+ IllegalArgumentError: If the value is not in the range 1-20.
642
+ """
643
+ self.__check_sync_ptr_not_null()
644
+ c.obx_sync_max_messages_in_flight(self.__c_sync_client_ptr, value)
645
+
646
+ def set_request_updates_mode(self, mode: SyncRequestUpdatesMode):
647
+ """Configures how sync updates are received from the server.
648
+
649
+ If automatic updates are turned off, they will need to be requested manually.
650
+ Must be called before start().
651
+
652
+ Args:
653
+ mode: The request updates mode to use.
654
+
655
+ Raises:
656
+ IllegalArgumentError: If mode is not a SyncRequestUpdatesMode value.
657
+ """
658
+ self.__check_sync_ptr_not_null()
659
+ c.obx_sync_request_updates_mode(self.__c_sync_client_ptr, _to_enum(SyncRequestUpdatesMode, mode))
660
+
661
+ def state(self) -> SyncState:
662
+ """Gets the current sync client state.
663
+
664
+ Returns:
665
+ The current SyncState of this client.
666
+ """
667
+ self.__check_sync_ptr_not_null()
668
+ c_state = c.obx_sync_state(self.__c_sync_client_ptr)
669
+ if c_state == c.SyncState.CREATED:
670
+ return SyncState.CREATED
671
+ elif c_state == c.SyncState.STARTED:
672
+ return SyncState.STARTED
673
+ elif c_state == c.SyncState.CONNECTED:
674
+ return SyncState.CONNECTED
675
+ elif c_state == c.SyncState.LOGGED_IN:
676
+ return SyncState.LOGGED_IN
677
+ elif c_state == c.SyncState.DISCONNECTED:
678
+ return SyncState.DISCONNECTED
679
+ elif c_state == c.SyncState.STOPPED:
680
+ return SyncState.STOPPED
681
+ elif c_state == c.SyncState.DEAD:
682
+ return SyncState.DEAD
683
+ else:
684
+ return SyncState.UNKNOWN
685
+
686
+ def start(self):
687
+ """Once the sync client is configured, you can start it to initiate synchronization.
688
+
689
+ This method triggers communication in the background and returns immediately.
690
+ The background thread will try to connect to the server, log-in and start
691
+ syncing data (depends on SyncRequestUpdatesMode). If the device, network or
692
+ server is currently offline, connection attempts will be retried later
693
+ automatically. If you haven't set the credentials in the options during
694
+ construction, call set_credentials() before start().
695
+ """
696
+ self.__check_sync_ptr_not_null()
697
+ c.obx_sync_start(self.__c_sync_client_ptr)
698
+
699
+ def stop(self):
700
+ """Stops this sync client. Does nothing if it is already stopped."""
701
+ self.__check_sync_ptr_not_null()
702
+ c.obx_sync_stop(self.__c_sync_client_ptr)
703
+
704
+ def trigger_reconnect(self) -> bool:
705
+ """Triggers a reconnection attempt immediately.
706
+
707
+ By default, an increasing backoff interval is used for reconnection attempts.
708
+ But sometimes the code using this API has additional knowledge and can
709
+ initiate a reconnection attempt sooner.
710
+
711
+ Returns:
712
+ True if a reconnect was actually triggered.
713
+ """
714
+ self.__check_sync_ptr_not_null()
715
+ return c.obx_sync_trigger_reconnect(self.__c_sync_client_ptr)
716
+
717
+ def request_updates(self, subscribe_for_future_pushes: bool) -> bool:
718
+ """Request updates since we last synchronized our database.
719
+
720
+ Additionally, you can subscribe for future pushes from the server, to let
721
+ it send us future updates as they come in.
722
+ Call cancel_updates() to stop the updates.
723
+
724
+ Args:
725
+ subscribe_for_future_pushes: If True, also subscribe for future pushes.
726
+
727
+ Returns:
728
+ True if the request was likely sent (e.g. the client is logged in); False if it was not sent
729
+ (and will not be sent in the future).
730
+ """
731
+ self.__check_sync_ptr_not_null()
732
+ return c.obx_sync_updates_request(self.__c_sync_client_ptr, subscribe_for_future_pushes)
733
+
734
+ def cancel_updates(self) -> bool:
735
+ """Cancel updates from the server so that it will stop sending updates.
736
+
737
+ See also request_updates().
738
+
739
+ Returns:
740
+ True if the cancellation was likely sent (e.g. the client is logged in); False if it was not sent
741
+ (and will not be sent in the future).
742
+ """
743
+ self.__check_sync_ptr_not_null()
744
+ return c.obx_sync_updates_cancel(self.__c_sync_client_ptr)
745
+
746
+ @staticmethod
747
+ def protocol_version() -> int:
748
+ """Returns the protocol version this client uses."""
749
+ return c.obx_sync_protocol_version()
750
+
751
+ def protocol_version_server(self) -> int:
752
+ """Returns the protocol version of the server after a connection is
753
+ established (or attempted), zero otherwise.
754
+ """
755
+ self.__check_sync_ptr_not_null()
756
+ return c.obx_sync_protocol_version_server(self.__c_sync_client_ptr)
757
+
758
+ def close(self):
759
+ """Closes and cleans up all resources used by this sync client.
760
+
761
+ It can no longer be used afterwards, make a new sync client instead.
762
+ Does nothing if this sync client has already been closed.
763
+ Also called when the store is closed. Note: the store keeps its client alive until either is closed.
764
+
765
+ Note: do not call this from a listener (it is called by the client's background thread, which this joins),
766
+ and do not use this client concurrently from other threads while closing.
767
+ """
768
+ with self.__close_lock: # Only one thread may close, e.g. the user and the store (its close listener)
769
+ c_sync_client_ptr = self.__c_sync_client_ptr
770
+ if c_sync_client_ptr is None:
771
+ return
772
+ self.__c_sync_client_ptr = None
773
+ self.__store._remove_close_listener(self.__on_store_close)
774
+ if self.__store._sync_client is self:
775
+ self.__store._sync_client = None
776
+ # Stops the client and joins its background thread; thus, no listener is called after this returns
777
+ c.obx_sync_close(c_sync_client_ptr)
778
+ # Now the C callbacks are no longer used and can be released
779
+ self.__c_callbacks = []
780
+
781
+ def is_closed(self) -> bool:
782
+ """Returns if this sync client is closed and can no longer be used."""
783
+ return self.__c_sync_client_ptr is None
784
+
785
+ def __c_callback(self, c_callback_type, callback: typing.Optional[typing.Callable]):
786
+ """Returns a C callback for the given Python function (keeps it referenced), or a NULL one if None."""
787
+ if callback is None:
788
+ return c_callback_type() # NULL function pointer (ctypes does not accept None for a CFUNCTYPE argument)
789
+ c_callback = c_callback_type(callback)
790
+ self.__c_callbacks.append(c_callback)
791
+ return c_callback
792
+
793
+ def set_login_listener(self, login_listener: typing.Optional[SyncLoginListener]):
794
+ """Sets a listener to observe login events (success/failure), replacing a previously set one.
795
+
796
+ Args:
797
+ login_listener: The listener to receive login events; None to remove the listener.
798
+ """
799
+ self.__check_sync_ptr_not_null()
800
+ c_login_listener = self.__c_callback(
801
+ c.OBX_sync_listener_login_t,
802
+ None if login_listener is None else (lambda arg: login_listener.on_logged_in()))
803
+ c_login_failure_listener = self.__c_callback(
804
+ c.OBX_sync_listener_login_failure_t,
805
+ None if login_listener is None else (lambda arg, sync_login_code: login_listener.on_login_failed(
806
+ _to_sync_code(sync_login_code))))
807
+ c.obx_sync_listener_login(self.__c_sync_client_ptr, c_login_listener, None)
808
+ c.obx_sync_listener_login_failure(self.__c_sync_client_ptr, c_login_failure_listener, None)
809
+
810
+ def set_connection_listener(self, connection_listener: typing.Optional[SyncConnectionListener]):
811
+ """Sets a listener to observe connection state changes (connect/disconnect), replacing a previously set one.
812
+
813
+ Args:
814
+ connection_listener: The listener to receive connection events; None to remove the listener.
815
+ """
816
+ self.__check_sync_ptr_not_null()
817
+ c_connect_listener = self.__c_callback(
818
+ c.OBX_sync_listener_connect_t,
819
+ None if connection_listener is None else (lambda arg: connection_listener.on_connected()))
820
+ c_disconnect_listener = self.__c_callback(
821
+ c.OBX_sync_listener_disconnect_t,
822
+ None if connection_listener is None else (lambda arg: connection_listener.on_disconnected()))
823
+ c.obx_sync_listener_connect(self.__c_sync_client_ptr, c_connect_listener, None)
824
+ c.obx_sync_listener_disconnect(self.__c_sync_client_ptr, c_disconnect_listener, None)
825
+
826
+ def set_completed_listener(self, completed_listener: typing.Optional[SyncCompletedListener]):
827
+ """Sets a listener to observe when synchronization has completed, replacing a previously set one.
828
+
829
+ Args:
830
+ completed_listener: The listener to receive "updates completed" events; None to remove the listener.
831
+ """
832
+ self.__check_sync_ptr_not_null()
833
+ c_complete_listener = self.__c_callback(
834
+ c.OBX_sync_listener_complete_t,
835
+ None if completed_listener is None else (lambda arg: completed_listener.on_updates_completed()))
836
+ c.obx_sync_listener_complete(self.__c_sync_client_ptr, c_complete_listener, None)
837
+
838
+ def set_listener(self, listener: typing.Optional[SyncListener]):
839
+ """Sets a listener for all Sync events, replacing any previously set login, completed, connection, change and
840
+ time listener (like calling each set_*_listener() with it).
841
+
842
+ Args:
843
+ listener: The listener to receive all events; None to remove all these listeners.
844
+ """
845
+ self.set_login_listener(listener)
846
+ self.set_completed_listener(listener)
847
+ self.set_connection_listener(listener)
848
+ self.set_change_listener(listener)
849
+ self.set_time_listener(listener)
850
+
851
+ def set_time_listener(self, time_listener: typing.Optional[SyncTimeListener]):
852
+ """Sets a listener to observe server time updates, replacing a previously set one.
853
+
854
+ Args:
855
+ time_listener: The listener to receive server time updates; None to remove the listener.
856
+ """
857
+ self.__check_sync_ptr_not_null()
858
+ c_time_listener = self.__c_callback(
859
+ c.OBX_sync_listener_server_time_t,
860
+ None if time_listener is None else (
861
+ lambda arg, timestamp_ns: time_listener.on_server_time_update(timestamp_ns)))
862
+ c.obx_sync_listener_server_time(self.__c_sync_client_ptr, c_time_listener, None)
863
+
864
+ def server_time_nanos(self) -> int:
865
+ """Estimates the current server time based on the last known server time.
866
+
867
+ Returns:
868
+ The Unix timestamp in nanoseconds (since epoch); 0 if there has not been a server contact yet and thus the
869
+ server's time is unknown.
870
+ """
871
+ self.__check_sync_ptr_not_null()
872
+ timestamp_ns = ctypes.c_int64(0)
873
+ c.obx_sync_time_server(self.__c_sync_client_ptr, ctypes.byref(timestamp_ns))
874
+ return timestamp_ns.value
875
+
876
+ def server_time_diff_nanos(self) -> int:
877
+ """Returns the estimated difference between the server time and the local time, i.e. server_time_nanos()
878
+ minus the current time (nanoseconds since epoch).
879
+
880
+ Returns:
881
+ The time difference in nanoseconds, e.g. positive if the server time is ahead of the local time; 0 if
882
+ there has not been a server contact yet and thus the server's time is unknown.
883
+ """
884
+ self.__check_sync_ptr_not_null()
885
+ diff_ns = ctypes.c_int64(0)
886
+ c.obx_sync_time_server_diff(self.__c_sync_client_ptr, ctypes.byref(diff_ns))
887
+ return diff_ns.value
888
+
889
+ def set_change_listener(self, change_listener: typing.Optional[SyncChangeListener]):
890
+ """Sets a listener to observe incoming data changes from the server, replacing a previously set one.
891
+
892
+ Args:
893
+ change_listener: The listener to receive change events; None to remove the listener.
894
+ """
895
+ self.__check_sync_ptr_not_null()
896
+ model = self.__store._model
897
+ entities_by_id = {entity._id: entity for entity in model.entities} if model is not None else {}
898
+
899
+ def c_change_callback(arg, sync_change_array_ptr):
900
+ sync_change_array = ctypes.cast(sync_change_array_ptr, ctypes.POINTER(OBX_sync_change_array)).contents
901
+ changes: typing.List[SyncChange] = []
902
+ for i in range(sync_change_array.count):
903
+ c_sync_change: c.OBX_sync_change = sync_change_array.list[i]
904
+ puts = []
905
+ if c_sync_change.puts:
906
+ c_puts_id_array: c.OBX_id_array = ctypes.cast(c_sync_change.puts, c.OBX_id_array_p).contents
907
+ puts = list(
908
+ ctypes.cast(c_puts_id_array.ids, ctypes.POINTER(c.obx_id * c_puts_id_array.count)).contents)
909
+ removals = []
910
+ if c_sync_change.removals:
911
+ c_removals_id_array: c.OBX_id_array = ctypes.cast(c_sync_change.removals, c.OBX_id_array_p).contents
912
+ removals = list(
913
+ ctypes.cast(c_removals_id_array.ids,
914
+ ctypes.POINTER(c.obx_id * c_removals_id_array.count)).contents)
915
+ changes.append(SyncChange(
916
+ entity_id=c_sync_change.entity_id,
917
+ puts=puts,
918
+ removals=removals,
919
+ entity=entities_by_id.get(c_sync_change.entity_id)
920
+ ))
921
+ change_listener.on_change(changes)
922
+
923
+ c_change_listener = self.__c_callback(c.OBX_sync_listener_change_t,
924
+ None if change_listener is None else c_change_callback)
925
+ c.obx_sync_listener_change(self.__c_sync_client_ptr, c_change_listener, None)
926
+
927
+ def wait_for_logged_in_state(self, timeout_millis: int) -> bool:
928
+ """Waits for the sync client to reach the logged-in state.
929
+
930
+ Args:
931
+ timeout_millis: Maximum time to wait in milliseconds (must be greater than 0).
932
+
933
+ Returns:
934
+ True if the logged-in state was reached within the given timeout; False if the timeout was reached or
935
+ a state was reached that is unlikely to result in a successful login (e.g. disconnected).
936
+ """
937
+ self.__check_sync_ptr_not_null()
938
+ return c.obx_sync_wait_for_logged_in_state(self.__c_sync_client_ptr, timeout_millis)
939
+
940
+ def put_filter_variable(self, name: str, value: str):
941
+ """Adds or replaces a Sync filter variable value for the given name.
942
+
943
+ Eventually, existing values for the same name are replaced.
944
+
945
+ Sync client filter variables can be used in server-side Sync filters to
946
+ filter out objects that do not match the filters. Variables put before login (e.g. before start()) are used
947
+ for the login; changes after login are pending until apply_filter_variables() is called. This allows to put or
948
+ remove multiple variables before applying all changes.
949
+
950
+ See also remove_filter_variable(), remove_all_filter_variables() and apply_filter_variables().
951
+
952
+ Args:
953
+ name: The name of the filter variable.
954
+ value: The value of the filter variable.
955
+ """
956
+ self.__check_sync_ptr_not_null()
957
+ c.obx_sync_filter_variables_put(self.__c_sync_client_ptr, name.encode('utf-8'), value.encode('utf-8'))
958
+
959
+ def remove_filter_variable(self, name: str):
960
+ """Removes a previously added Sync filter variable value.
961
+
962
+ Note: if the client is already logged in, this change is not applied until apply_filter_variables() is called
963
+ (see put_filter_variable() for details).
964
+
965
+ See also put_filter_variable(), remove_all_filter_variables() and apply_filter_variables().
966
+
967
+ Args:
968
+ name: The name of the filter variable to remove.
969
+ """
970
+ self.__check_sync_ptr_not_null()
971
+ c.obx_sync_filter_variables_remove(self.__c_sync_client_ptr, name.encode('utf-8'))
972
+
973
+ def remove_all_filter_variables(self):
974
+ """Removes all previously added Sync filter variable values.
975
+
976
+ Note: if the client is already logged in, this change is not applied until apply_filter_variables() is called
977
+ (see put_filter_variable() for details).
978
+
979
+ See also put_filter_variable(), remove_filter_variable() and apply_filter_variables().
980
+ """
981
+ self.__check_sync_ptr_not_null()
982
+ c.obx_sync_filter_variables_remove_all(self.__c_sync_client_ptr)
983
+
984
+ def apply_filter_variables(self):
985
+ """Applies all pending filter variable changes (put or removed after login).
986
+
987
+ If the client is connected, sends the updated variables to the server: the client then reconnects and the
988
+ server brings the client's data in line with the new variables. If the client is not connected, the updated
989
+ variables are used with the next login.
990
+
991
+ See also put_filter_variable(), remove_filter_variable() and remove_all_filter_variables().
992
+ """
993
+ self.__check_sync_ptr_not_null()
994
+ c.obx_sync_filter_variables_apply(self.__c_sync_client_ptr)
995
+
996
+ def outgoing_message_count(self, limit: int = 0) -> int:
997
+ """Count the number of messages in the outgoing queue, i.e. those waiting
998
+ to be sent to the server.
999
+
1000
+ By default, counts all messages without any limitation. For a lower number
1001
+ pass a limit that's enough for your app logic.
1002
+
1003
+ Note: This call uses a (read) transaction internally:
1004
+ 1) It's not just a "cheap" return of a single number. While this will
1005
+ still be fast, avoid calling this function excessively.
1006
+ 2) The result follows transaction view semantics, thus it may not always
1007
+ match the actual value.
1008
+
1009
+ Args:
1010
+ limit: Optional limit for counting messages. Default is 0 (no limit).
1011
+
1012
+ Returns:
1013
+ The number of messages in the outgoing queue.
1014
+ """
1015
+ self.__check_sync_ptr_not_null()
1016
+ outgoing_message_count = ctypes.c_uint64(0)
1017
+ c.obx_sync_outgoing_message_count(self.__c_sync_client_ptr, limit, ctypes.byref(outgoing_message_count))
1018
+ return outgoing_message_count.value
1019
+
1020
+
1021
+ class Sync:
1022
+ """ObjectBox Sync makes data available and synchronized across devices,
1023
+ online and offline.
1024
+
1025
+ Start a client using Sync.client() and connect to a remote server.
1026
+ """
1027
+
1028
+ @staticmethod
1029
+ def is_available() -> bool:
1030
+ """Returns True if the loaded ObjectBox native library supports Sync."""
1031
+ return c.obx_has_feature(c.Feature.Sync)
1032
+
1033
+ @staticmethod
1034
+ def sync_clock_timestamp(sync_clock_value: int) -> int:
1035
+ """Gets the "raw" timestamp (milliseconds since epoch) from the given sync clock value.
1036
+
1037
+ Note that sync clock values are assigned by ObjectBox and are not to be interpreted on their own. Use this
1038
+ function to extract a meaningful timestamp from a sync clock value (e.g. of a SyncClock property).
1039
+ """
1040
+ return c.obx_sync_clock_timestamp(sync_clock_value)
1041
+
1042
+ @staticmethod
1043
+ def sync_clock_timestamp_corrected(sync_clock_value: int) -> int:
1044
+ """Gets the corrected timestamp (milliseconds since epoch) from the given sync clock value.
1045
+
1046
+ Like sync_clock_timestamp(), but applies any time correction if present in the sync clock value. However, for
1047
+ most cases, it returns the same value as sync_clock_timestamp().
1048
+ """
1049
+ return c.obx_sync_clock_timestamp_corrected(sync_clock_value)
1050
+
1051
+ @staticmethod
1052
+ def client(
1053
+ store: Store,
1054
+ url: typing.Union[str, typing.List[str]],
1055
+ credentials: typing.Union[SyncCredentials, typing.List[SyncCredentials]],
1056
+ filter_variables: typing.Optional[typing.Dict[str, str]] = None,
1057
+ *,
1058
+ flags: int = 0,
1059
+ certificate_paths: typing.Optional[typing.List[str]] = None
1060
+ ) -> SyncClient:
1061
+ """Creates a Sync client associated with the given store and configures it
1062
+ with the given options.
1063
+
1064
+ This does not initiate any connection attempts yet, call SyncClient.start()
1065
+ to do so.
1066
+
1067
+ Before SyncClient.start(), you can still configure some aspects of the
1068
+ client, e.g. its request updates mode.
1069
+
1070
+ To configure Sync filter variables, pass variable names mapped to their
1071
+ value to filter_variables. Sync client filter variables can be used in
1072
+ server-side Sync filters to filter out objects that do not match the filter.
1073
+
1074
+ To adjust the client behavior, pass SyncFlags to flags, e.g. SyncFlags.REMOVE_WITH_OBJECT_DATA (recommended
1075
+ with Sync filters).
1076
+
1077
+ For encrypted connections ("wss://") with e.g. self-signed certificates in a local development environment or
1078
+ custom certificate authorities (CAs), pass paths of certificate files to certificate_paths. By default, the
1079
+ certificate authorities trusted by the system are used.
1080
+
1081
+ Args:
1082
+ store: The ObjectBox store to sync.
1083
+ url: The URL of the Sync server to connect to, e.g. "ws://127.0.0.1:9999" ("wss://" for an encrypted
1084
+ connection); or a list of URLs to work with multiple servers (a random URL is selected for each
1085
+ connection attempt).
1086
+ credentials: The credentials to use for authentication; or a list of credentials (see
1087
+ SyncClient.set_credentials()).
1088
+ filter_variables: Optional dictionary of filter variable names to values.
1089
+ flags: SyncFlags to adjust the client behavior (combine with ``|``).
1090
+ certificate_paths: Paths of SSL certificates (in the local file system) for encrypted connections.
1091
+
1092
+ Returns:
1093
+ A configured SyncClient instance.
1094
+
1095
+ Raises:
1096
+ IllegalStateError: If a sync client is already active (not closed) for the given store, or the store is
1097
+ closed.
1098
+ FeatureNotAvailableError: If the loaded ObjectBox library does not support Sync.
1099
+ """
1100
+ urls = [url] if isinstance(url, str) else list(url)
1101
+ return SyncClient._create(store, urls, credentials, filter_variables, flags, certificate_paths)