usearch 2.2.0 → 2.3.0
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.
- package/.github/workflows/prerelease.yml +12 -3
- package/Cargo.toml +1 -1
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +1 -0
- package/include/usearch/index.hpp +48 -12
- package/include/usearch/index_dense.hpp +43 -20
- package/include/usearch/index_plugins.hpp +59 -13
- package/package.json +1 -1
- package/python/lib.cpp +133 -61
- package/python/usearch/index.py +112 -79
- package/simsimd/include/simsimd/simsimd.h +106 -40
- package/wasmer.toml +1 -1
package/python/usearch/index.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from inspect import signature
|
|
2
3
|
|
|
3
4
|
# The purpose of this file is to provide Pythonic wrapper on top
|
|
4
5
|
# the native precompiled CPython module. It improves compatibility
|
|
@@ -8,6 +9,7 @@ import os
|
|
|
8
9
|
import math
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from typing import (
|
|
12
|
+
Any,
|
|
11
13
|
Optional,
|
|
12
14
|
Union,
|
|
13
15
|
NamedTuple,
|
|
@@ -72,6 +74,17 @@ NoneType = type(None)
|
|
|
72
74
|
|
|
73
75
|
PathOrBuffer = Union[str, os.PathLike, bytes]
|
|
74
76
|
|
|
77
|
+
ProgressCallback = Callable[[int, int], bool]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _match_signature(
|
|
81
|
+
func: Callable[[Any], Any], arg_types: List[type], ret_type: type
|
|
82
|
+
) -> bool:
|
|
83
|
+
assert callable(func), "Not callable"
|
|
84
|
+
sig = signature(func)
|
|
85
|
+
param_types = [param.annotation for param in sig.parameters.values()]
|
|
86
|
+
return param_types == arg_types and sig.return_annotation == ret_type
|
|
87
|
+
|
|
75
88
|
|
|
76
89
|
def _normalize_dtype(
|
|
77
90
|
dtype,
|
|
@@ -153,12 +166,16 @@ def _search_in_compiled(
|
|
|
153
166
|
vectors: np.ndarray,
|
|
154
167
|
*,
|
|
155
168
|
log: Union[str, bool],
|
|
156
|
-
|
|
169
|
+
progress: Optional[ProgressCallback],
|
|
157
170
|
**kwargs,
|
|
158
171
|
) -> Union[Matches, BatchMatches]:
|
|
159
172
|
#
|
|
160
173
|
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
161
174
|
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
175
|
+
assert not progress or _match_signature(
|
|
176
|
+
progress, [int, int], bool
|
|
177
|
+
), "Invalid callback"
|
|
178
|
+
|
|
162
179
|
if vectors.ndim == 1:
|
|
163
180
|
vectors = vectors.reshape(1, len(vectors))
|
|
164
181
|
count_vectors = vectors.shape[0]
|
|
@@ -168,42 +185,25 @@ def _search_in_compiled(
|
|
|
168
185
|
) -> Union[BatchMatches, Matches]:
|
|
169
186
|
return batch_matches[0] if count_vectors == 1 else batch_matches
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if batch_size:
|
|
175
|
-
tasks = [
|
|
176
|
-
vectors[start_row : start_row + batch_size, :]
|
|
177
|
-
for start_row in range(0, count_vectors, batch_size)
|
|
178
|
-
]
|
|
179
|
-
tasks_matches = []
|
|
188
|
+
# Create progress bar if needed
|
|
189
|
+
if log:
|
|
180
190
|
name = log if isinstance(log, str) else "Search"
|
|
181
191
|
pbar = tqdm(
|
|
182
|
-
tasks,
|
|
183
192
|
desc=name,
|
|
184
193
|
total=count_vectors,
|
|
185
194
|
unit="vector",
|
|
186
|
-
disable=log is False,
|
|
187
195
|
)
|
|
188
|
-
for vectors in tasks:
|
|
189
|
-
tuple_ = compiled_callable(vectors, **kwargs)
|
|
190
|
-
tasks_matches.append(BatchMatches(*tuple_))
|
|
191
|
-
pbar.update(vectors.shape[0])
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
keys=np.vstack([m.keys for m in tasks_matches]),
|
|
197
|
-
distances=np.vstack([m.distances for m in tasks_matches]),
|
|
198
|
-
counts=np.concatenate([m.counts for m in tasks_matches], axis=None),
|
|
199
|
-
visited_members=sum([m.visited_members for m in tasks_matches]),
|
|
200
|
-
computed_distances=sum([m.computed_distances for m in tasks_matches]),
|
|
201
|
-
)
|
|
202
|
-
)
|
|
197
|
+
def update_progress_bar(processed: int, total: int) -> bool:
|
|
198
|
+
pbar.update(processed - pbar.n)
|
|
199
|
+
return progress if progress else True
|
|
203
200
|
|
|
201
|
+
tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
|
|
202
|
+
pbar.close()
|
|
204
203
|
else:
|
|
205
204
|
tuple_ = compiled_callable(vectors, **kwargs)
|
|
206
|
-
|
|
205
|
+
|
|
206
|
+
return distil_batch(BatchMatches(*tuple_))
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _add_to_compiled(
|
|
@@ -214,9 +214,13 @@ def _add_to_compiled(
|
|
|
214
214
|
copy: bool,
|
|
215
215
|
threads: int,
|
|
216
216
|
log: Union[str, bool],
|
|
217
|
-
|
|
217
|
+
progress: Optional[ProgressCallback],
|
|
218
218
|
) -> Union[int, np.ndarray]:
|
|
219
|
+
#
|
|
219
220
|
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
221
|
+
assert not progress or _match_signature(
|
|
222
|
+
progress, [int, int], bool
|
|
223
|
+
), "Invalid callback"
|
|
220
224
|
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
221
225
|
if vectors.ndim == 1:
|
|
222
226
|
vectors = vectors.reshape(1, len(vectors))
|
|
@@ -235,37 +239,29 @@ def _add_to_compiled(
|
|
|
235
239
|
|
|
236
240
|
assert len(keys) == count_vectors
|
|
237
241
|
|
|
238
|
-
#
|
|
239
|
-
if log
|
|
240
|
-
batch_size = int(math.ceil(count_vectors / 100))
|
|
241
|
-
|
|
242
|
-
# Split into batches and log progress, if needed
|
|
243
|
-
if batch_size:
|
|
244
|
-
keys = [
|
|
245
|
-
keys[start_row : start_row + batch_size]
|
|
246
|
-
for start_row in range(0, count_vectors, batch_size)
|
|
247
|
-
]
|
|
248
|
-
vectors = [
|
|
249
|
-
vectors[start_row : start_row + batch_size, :]
|
|
250
|
-
for start_row in range(0, count_vectors, batch_size)
|
|
251
|
-
]
|
|
252
|
-
tasks = zip(keys, vectors)
|
|
242
|
+
# Create progress bar if needed
|
|
243
|
+
if log:
|
|
253
244
|
name = log if isinstance(log, str) else "Add"
|
|
254
245
|
pbar = tqdm(
|
|
255
|
-
tasks,
|
|
256
246
|
desc=name,
|
|
257
247
|
total=count_vectors,
|
|
258
248
|
unit="vector",
|
|
259
|
-
disable=log is False,
|
|
260
249
|
)
|
|
261
|
-
for keys, vectors in tasks:
|
|
262
|
-
compiled.add_many(keys, vectors, copy=copy, threads=threads)
|
|
263
|
-
pbar.update(len(keys))
|
|
264
250
|
|
|
265
|
-
|
|
251
|
+
def update_progress_bar(processed: int, total: int) -> bool:
|
|
252
|
+
pbar.update(processed - pbar.n)
|
|
253
|
+
return progress(processed, total) if progress else True
|
|
266
254
|
|
|
255
|
+
compiled.add_many(
|
|
256
|
+
keys,
|
|
257
|
+
vectors,
|
|
258
|
+
copy=copy,
|
|
259
|
+
threads=threads,
|
|
260
|
+
progress=update_progress_bar,
|
|
261
|
+
)
|
|
262
|
+
pbar.close()
|
|
267
263
|
else:
|
|
268
|
-
compiled.add_many(keys, vectors, copy=copy, threads=threads)
|
|
264
|
+
compiled.add_many(keys, vectors, copy=copy, threads=threads, progress=progress)
|
|
269
265
|
|
|
270
266
|
return keys
|
|
271
267
|
|
|
@@ -625,7 +621,7 @@ class Index:
|
|
|
625
621
|
copy: bool = True,
|
|
626
622
|
threads: int = 0,
|
|
627
623
|
log: Union[str, bool] = False,
|
|
628
|
-
|
|
624
|
+
progress: Optional[ProgressCallback] = None,
|
|
629
625
|
) -> Union[int, np.ndarray]:
|
|
630
626
|
"""Inserts one or move vectors into the index.
|
|
631
627
|
|
|
@@ -651,8 +647,8 @@ class Index:
|
|
|
651
647
|
:type threads: int, defaults to 0
|
|
652
648
|
:param log: Whether to print the progress bar
|
|
653
649
|
:type log: Union[str, bool], defaults to False
|
|
654
|
-
:param
|
|
655
|
-
:type
|
|
650
|
+
:param progress: Callback to report stats of the progress and control it
|
|
651
|
+
:type progress: Optional[ProgressCallback], defaults to None
|
|
656
652
|
:return: Inserted key or keys
|
|
657
653
|
:type: Union[int, np.ndarray]
|
|
658
654
|
"""
|
|
@@ -663,7 +659,7 @@ class Index:
|
|
|
663
659
|
copy=copy,
|
|
664
660
|
threads=threads,
|
|
665
661
|
log=log,
|
|
666
|
-
|
|
662
|
+
progress=progress,
|
|
667
663
|
)
|
|
668
664
|
|
|
669
665
|
def search(
|
|
@@ -675,7 +671,7 @@ class Index:
|
|
|
675
671
|
threads: int = 0,
|
|
676
672
|
exact: bool = False,
|
|
677
673
|
log: Union[str, bool] = False,
|
|
678
|
-
|
|
674
|
+
progress: Optional[ProgressCallback] = None,
|
|
679
675
|
) -> Union[Matches, BatchMatches]:
|
|
680
676
|
"""
|
|
681
677
|
Performs approximate nearest neighbors search for one or more queries.
|
|
@@ -690,8 +686,8 @@ class Index:
|
|
|
690
686
|
:type exact: bool, defaults to False
|
|
691
687
|
:param log: Whether to print the progress bar, default to False
|
|
692
688
|
:type log: Union[str, bool], optional
|
|
693
|
-
:param
|
|
694
|
-
:type
|
|
689
|
+
:param progress: Callback to report stats of the progress and control it
|
|
690
|
+
:type progress: Optional[ProgressCallback], defaults to None
|
|
695
691
|
:return: Matches for one or more queries
|
|
696
692
|
:rtype: Union[Matches, BatchMatches]
|
|
697
693
|
"""
|
|
@@ -701,11 +697,11 @@ class Index:
|
|
|
701
697
|
vectors,
|
|
702
698
|
# Batch scheduling:
|
|
703
699
|
log=log,
|
|
704
|
-
batch_size=batch_size,
|
|
705
700
|
# Search constraints:
|
|
706
701
|
count=count,
|
|
707
702
|
exact=exact,
|
|
708
703
|
threads=threads,
|
|
704
|
+
progress=progress,
|
|
709
705
|
)
|
|
710
706
|
|
|
711
707
|
def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
|
|
@@ -932,37 +928,57 @@ class Index:
|
|
|
932
928
|
self._compiled.expansion_search = v
|
|
933
929
|
|
|
934
930
|
def save(
|
|
935
|
-
self,
|
|
931
|
+
self,
|
|
932
|
+
path_or_buffer: Union[str, os.PathLike, NoneType] = None,
|
|
933
|
+
progress: Optional[ProgressCallback] = None,
|
|
936
934
|
) -> Optional[bytes]:
|
|
935
|
+
assert not progress or _match_signature(
|
|
936
|
+
progress, [int, int], bool
|
|
937
|
+
), "Invalid callback signature"
|
|
938
|
+
|
|
937
939
|
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
938
940
|
if path_or_buffer is None:
|
|
939
|
-
return self._compiled.save_index_to_buffer()
|
|
941
|
+
return self._compiled.save_index_to_buffer(progress)
|
|
940
942
|
else:
|
|
941
|
-
self._compiled.save_index_to_path(os.fspath(path_or_buffer))
|
|
943
|
+
self._compiled.save_index_to_path(os.fspath(path_or_buffer), progress)
|
|
944
|
+
|
|
945
|
+
def load(
|
|
946
|
+
self,
|
|
947
|
+
path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None,
|
|
948
|
+
progress: Optional[ProgressCallback] = None,
|
|
949
|
+
):
|
|
950
|
+
assert not progress or _match_signature(
|
|
951
|
+
progress, [int, int], bool
|
|
952
|
+
), "Invalid callback signature"
|
|
942
953
|
|
|
943
|
-
def load(self, path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None):
|
|
944
954
|
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
945
955
|
if path_or_buffer is None:
|
|
946
956
|
raise Exception("Define the source")
|
|
947
957
|
if isinstance(path_or_buffer, bytearray):
|
|
948
958
|
path_or_buffer = bytes(path_or_buffer)
|
|
949
959
|
if isinstance(path_or_buffer, bytes):
|
|
950
|
-
self._compiled.load_index_from_buffer(path_or_buffer)
|
|
960
|
+
self._compiled.load_index_from_buffer(path_or_buffer, progress)
|
|
951
961
|
else:
|
|
952
|
-
self._compiled.load_index_from_path(os.fspath(path_or_buffer))
|
|
962
|
+
self._compiled.load_index_from_path(os.fspath(path_or_buffer), progress)
|
|
953
963
|
|
|
954
964
|
def view(
|
|
955
|
-
self,
|
|
965
|
+
self,
|
|
966
|
+
path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None,
|
|
967
|
+
progress: Optional[ProgressCallback] = None,
|
|
956
968
|
):
|
|
969
|
+
assert not progress or _match_signature(
|
|
970
|
+
progress, [int, int], bool
|
|
971
|
+
), "Invalid callback signature"
|
|
972
|
+
|
|
957
973
|
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
958
974
|
if path_or_buffer is None:
|
|
959
975
|
raise Exception("Define the source")
|
|
960
976
|
if isinstance(path_or_buffer, bytearray):
|
|
961
977
|
path_or_buffer = bytes(path_or_buffer)
|
|
962
978
|
if isinstance(path_or_buffer, bytes):
|
|
963
|
-
self._compiled.view_index_from_buffer(path_or_buffer)
|
|
979
|
+
self._compiled.view_index_from_buffer(path_or_buffer, progress)
|
|
964
980
|
else:
|
|
965
|
-
self._compiled.view_index_from_path(os.fspath(path_or_buffer))
|
|
981
|
+
self._compiled.view_index_from_path(os.fspath(path_or_buffer), progress)
|
|
966
982
|
|
|
967
983
|
def clear(self):
|
|
968
984
|
"""Erases all the vectors from the index, preserving the space for future insertions."""
|
|
@@ -993,6 +1009,7 @@ class Index:
|
|
|
993
1009
|
other: Index,
|
|
994
1010
|
max_proposals: int = 0,
|
|
995
1011
|
exact: bool = False,
|
|
1012
|
+
progress: Optional[ProgressCallback] = None,
|
|
996
1013
|
) -> Dict[Key, Key]:
|
|
997
1014
|
"""Performs "Semantic Join" or pairwise matching between `self` & `other` index.
|
|
998
1015
|
Is different from `search`, as no collisions are allowed in resulting pairs.
|
|
@@ -1005,13 +1022,20 @@ class Index:
|
|
|
1005
1022
|
:type max_proposals: int, optional
|
|
1006
1023
|
:param exact: Controls if underlying `search` should be exact, defaults to False
|
|
1007
1024
|
:type exact: bool, optional
|
|
1025
|
+
:param progress: Callback to report stats of the progress and control it
|
|
1026
|
+
:type progress: Optional[ProgressCallback], defaults to None
|
|
1008
1027
|
:return: Mapping from keys of `self` to keys of `other`
|
|
1009
1028
|
:rtype: Dict[Key, Key]
|
|
1010
1029
|
"""
|
|
1030
|
+
assert not progress or _match_signature(
|
|
1031
|
+
progress, [int, int], bool
|
|
1032
|
+
), "Invalid callback signature"
|
|
1033
|
+
|
|
1011
1034
|
return self._compiled.join(
|
|
1012
1035
|
other=other._compiled,
|
|
1013
1036
|
max_proposals=max_proposals,
|
|
1014
1037
|
exact=exact,
|
|
1038
|
+
progress=progress,
|
|
1015
1039
|
)
|
|
1016
1040
|
|
|
1017
1041
|
def cluster(
|
|
@@ -1023,7 +1047,7 @@ class Index:
|
|
|
1023
1047
|
max_count: Optional[int] = None,
|
|
1024
1048
|
threads: int = 0,
|
|
1025
1049
|
log: Union[str, bool] = False,
|
|
1026
|
-
|
|
1050
|
+
progress: Optional[ProgressCallback] = None,
|
|
1027
1051
|
) -> Clustering:
|
|
1028
1052
|
"""
|
|
1029
1053
|
Clusters already indexed or provided `vectors`, mapping them to various centroids.
|
|
@@ -1037,11 +1061,15 @@ class Index:
|
|
|
1037
1061
|
:type threads: int, defaults to 0
|
|
1038
1062
|
:param log: Whether to print the progress bar
|
|
1039
1063
|
:type log: Union[str, bool], defaults to False
|
|
1040
|
-
:param
|
|
1041
|
-
:type
|
|
1064
|
+
:param progress: Callback to report stats of the progress and control it
|
|
1065
|
+
:type progress: Optional[ProgressCallback], defaults to None
|
|
1042
1066
|
:return: Matches for one or more queries
|
|
1043
1067
|
:rtype: Union[Matches, BatchMatches]
|
|
1044
1068
|
"""
|
|
1069
|
+
assert not progress or _match_signature(
|
|
1070
|
+
progress, [int, int], bool
|
|
1071
|
+
), "Invalid callback signature"
|
|
1072
|
+
|
|
1045
1073
|
if min_count is None:
|
|
1046
1074
|
min_count = 0
|
|
1047
1075
|
if max_count is None:
|
|
@@ -1054,6 +1082,7 @@ class Index:
|
|
|
1054
1082
|
min_count=min_count,
|
|
1055
1083
|
max_count=max_count,
|
|
1056
1084
|
threads=threads,
|
|
1085
|
+
progress=progress,
|
|
1057
1086
|
)
|
|
1058
1087
|
else:
|
|
1059
1088
|
if keys is None:
|
|
@@ -1066,6 +1095,7 @@ class Index:
|
|
|
1066
1095
|
min_count=min_count,
|
|
1067
1096
|
max_count=max_count,
|
|
1068
1097
|
threads=threads,
|
|
1098
|
+
progress=progress,
|
|
1069
1099
|
)
|
|
1070
1100
|
|
|
1071
1101
|
batch_matches = BatchMatches(*results)
|
|
@@ -1231,17 +1261,18 @@ class Indexes:
|
|
|
1231
1261
|
*,
|
|
1232
1262
|
threads: int = 0,
|
|
1233
1263
|
exact: bool = False,
|
|
1264
|
+
progress: Optional[ProgressCallback] = None,
|
|
1234
1265
|
):
|
|
1235
1266
|
return _search_in_compiled(
|
|
1236
1267
|
self._compiled.search_many,
|
|
1237
1268
|
vectors,
|
|
1238
1269
|
# Batch scheduling:
|
|
1239
1270
|
log=False,
|
|
1240
|
-
batch_size=None,
|
|
1241
1271
|
# Search constraints:
|
|
1242
1272
|
count=count,
|
|
1243
1273
|
exact=exact,
|
|
1244
1274
|
threads=threads,
|
|
1275
|
+
progress=progress,
|
|
1245
1276
|
)
|
|
1246
1277
|
|
|
1247
1278
|
|
|
@@ -1254,7 +1285,7 @@ def search(
|
|
|
1254
1285
|
exact: bool = False,
|
|
1255
1286
|
threads: int = 0,
|
|
1256
1287
|
log: Union[str, bool] = False,
|
|
1257
|
-
|
|
1288
|
+
progress: Optional[ProgressCallback] = None,
|
|
1258
1289
|
) -> Union[Matches, BatchMatches]:
|
|
1259
1290
|
"""Shortcut for search, that can avoid index construction. Particularly useful for
|
|
1260
1291
|
tiny datasets, where brute-force exact search works fast enough.
|
|
@@ -1279,12 +1310,14 @@ def search(
|
|
|
1279
1310
|
:type exact: bool, optional
|
|
1280
1311
|
:param log: Whether to print the progress bar, default to False
|
|
1281
1312
|
:type log: Union[str, bool], optional
|
|
1282
|
-
:param
|
|
1283
|
-
:type
|
|
1284
|
-
|
|
1313
|
+
:param progress: Callback to report stats of the progress and control it
|
|
1314
|
+
:type progress: Optional[ProgressCallback], defaults to None
|
|
1285
1315
|
:return: Matches for one or more queries
|
|
1286
1316
|
:rtype: Union[Matches, BatchMatches]
|
|
1287
1317
|
"""
|
|
1318
|
+
assert not progress or _match_signature(
|
|
1319
|
+
progress, [int, int], bool
|
|
1320
|
+
), "Invalid callback signature"
|
|
1288
1321
|
assert dataset.ndim == 2, "Dataset must be a matrix, with a vector in each row"
|
|
1289
1322
|
|
|
1290
1323
|
if not exact:
|
|
@@ -1298,14 +1331,14 @@ def search(
|
|
|
1298
1331
|
dataset,
|
|
1299
1332
|
threads=threads,
|
|
1300
1333
|
log=log,
|
|
1301
|
-
|
|
1334
|
+
progress=progress,
|
|
1302
1335
|
)
|
|
1303
1336
|
return index.search(
|
|
1304
1337
|
query,
|
|
1305
1338
|
count,
|
|
1306
1339
|
threads=threads,
|
|
1307
1340
|
log=log,
|
|
1308
|
-
|
|
1341
|
+
progress=progress,
|
|
1309
1342
|
)
|
|
1310
1343
|
|
|
1311
1344
|
metric = _normalize_metric(metric)
|
|
@@ -1329,8 +1362,8 @@ def search(
|
|
|
1329
1362
|
dataset,
|
|
1330
1363
|
query,
|
|
1331
1364
|
metric_kind=metric_kind,
|
|
1332
|
-
metric_pointer=metric_pointer,
|
|
1333
1365
|
metric_signature=metric_signature,
|
|
1366
|
+
metric_pointer=metric_pointer,
|
|
1334
1367
|
**kwargs,
|
|
1335
1368
|
)
|
|
1336
1369
|
|
|
@@ -1339,8 +1372,8 @@ def search(
|
|
|
1339
1372
|
query,
|
|
1340
1373
|
# Batch scheduling:
|
|
1341
1374
|
log=log,
|
|
1342
|
-
batch_size=batch_size,
|
|
1343
1375
|
# Search constraints:
|
|
1344
1376
|
count=count,
|
|
1345
1377
|
threads=threads,
|
|
1378
|
+
progress=progress,
|
|
1346
1379
|
)
|