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/__init__.py +145 -0
- objectbox/box.py +294 -0
- objectbox/builder.py +42 -0
- objectbox/c.py +1572 -0
- objectbox/condition.py +300 -0
- objectbox/exceptions.py +238 -0
- objectbox/experimental.py +78 -0
- objectbox/logger.py +18 -0
- objectbox/model/__init__.py +63 -0
- objectbox/model/codegen.py +399 -0
- objectbox/model/entity.py +490 -0
- objectbox/model/fast_builder.py +324 -0
- objectbox/model/idsync.py +401 -0
- objectbox/model/iduid.py +32 -0
- objectbox/model/model.py +158 -0
- objectbox/model/properties.py +865 -0
- objectbox/objectbox.py +24 -0
- objectbox/query.py +468 -0
- objectbox/query_builder.py +450 -0
- objectbox/store.py +400 -0
- objectbox/store_options.py +136 -0
- objectbox/sync.py +1101 -0
- objectbox/transaction.py +60 -0
- objectbox/utils.py +60 -0
- objectbox/version.py +39 -0
- objectbox_api-5.0.0.dist-info/METADATA +63 -0
- objectbox_api-5.0.0.dist-info/RECORD +30 -0
- objectbox_api-5.0.0.dist-info/WHEEL +5 -0
- objectbox_api-5.0.0.dist-info/licenses/LICENSE +202 -0
- objectbox_api-5.0.0.dist-info/top_level.txt +1 -0
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)
|