chromadb 3.2.2 → 3.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/dist/chromadb.d.ts +45 -8
- package/dist/chromadb.legacy-esm.js +404 -177
- package/dist/chromadb.mjs +404 -177
- package/dist/chromadb.mjs.map +1 -1
- package/dist/cjs/chromadb.cjs +404 -177
- package/dist/cjs/chromadb.cjs.map +1 -1
- package/dist/cjs/chromadb.d.cts +45 -8
- package/package.json +6 -6
- package/src/admin-client.ts +7 -7
- package/src/api/sdk.gen.ts +360 -135
- package/src/api/types.gen.ts +152 -58
- package/src/chroma-client.ts +18 -13
- package/src/collection.ts +18 -18
- package/src/execution/expression/key.ts +27 -6
- package/src/types.ts +28 -5
- package/src/utils.ts +72 -17
package/src/chroma-client.ts
CHANGED
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
deserializeMetadata,
|
|
8
8
|
serializeMetadata,
|
|
9
9
|
} from "./utils";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AuthenticationService,
|
|
12
|
+
CollectionService,
|
|
13
|
+
SystemService,
|
|
14
|
+
ChecklistResponse,
|
|
15
|
+
} from "./api";
|
|
11
16
|
import { CollectionMetadata, UserIdentity } from "./types";
|
|
12
17
|
import { Collection, CollectionImpl } from "./collection";
|
|
13
18
|
import { EmbeddingFunction, getEmbeddingFunction } from "./embedding-function";
|
|
@@ -207,7 +212,7 @@ export class ChromaClient {
|
|
|
207
212
|
* @returns Promise resolving to user identity data
|
|
208
213
|
*/
|
|
209
214
|
public async getUserIdentity(): Promise<UserIdentity> {
|
|
210
|
-
const { data } = await
|
|
215
|
+
const { data } = await AuthenticationService.getUserIdentity({
|
|
211
216
|
client: this.apiClient,
|
|
212
217
|
});
|
|
213
218
|
return data;
|
|
@@ -218,7 +223,7 @@ export class ChromaClient {
|
|
|
218
223
|
* @returns Promise resolving to the server's nanosecond heartbeat timestamp
|
|
219
224
|
*/
|
|
220
225
|
public async heartbeat(): Promise<number> {
|
|
221
|
-
const { data } = await
|
|
226
|
+
const { data } = await SystemService.heartbeat({
|
|
222
227
|
client: this.apiClient,
|
|
223
228
|
});
|
|
224
229
|
return data["nanosecond heartbeat"];
|
|
@@ -239,7 +244,7 @@ export class ChromaClient {
|
|
|
239
244
|
): Promise<Collection[]> {
|
|
240
245
|
const { limit = 100, offset = 0 } = args || {};
|
|
241
246
|
|
|
242
|
-
const { data } = await
|
|
247
|
+
const { data } = await CollectionService.listCollections({
|
|
243
248
|
client: this.apiClient,
|
|
244
249
|
path: await this._path(),
|
|
245
250
|
query: { limit, offset },
|
|
@@ -282,7 +287,7 @@ export class ChromaClient {
|
|
|
282
287
|
* @returns Promise resolving to the collection count
|
|
283
288
|
*/
|
|
284
289
|
public async countCollections(): Promise<number> {
|
|
285
|
-
const { data } = await
|
|
290
|
+
const { data } = await CollectionService.countCollections({
|
|
286
291
|
client: this.apiClient,
|
|
287
292
|
path: await this._path(),
|
|
288
293
|
});
|
|
@@ -320,7 +325,7 @@ export class ChromaClient {
|
|
|
320
325
|
schema,
|
|
321
326
|
});
|
|
322
327
|
|
|
323
|
-
const { data } = await
|
|
328
|
+
const { data } = await CollectionService.createCollection({
|
|
324
329
|
client: this.apiClient,
|
|
325
330
|
path: await this._path(),
|
|
326
331
|
body: {
|
|
@@ -376,7 +381,7 @@ export class ChromaClient {
|
|
|
376
381
|
name: string;
|
|
377
382
|
embeddingFunction?: EmbeddingFunction;
|
|
378
383
|
}): Promise<Collection> {
|
|
379
|
-
const { data } = await
|
|
384
|
+
const { data } = await CollectionService.getCollection({
|
|
380
385
|
client: this.apiClient,
|
|
381
386
|
path: { ...(await this._path()), collection_id: name },
|
|
382
387
|
});
|
|
@@ -413,7 +418,7 @@ export class ChromaClient {
|
|
|
413
418
|
* @throws Error if the collection does not exist
|
|
414
419
|
*/
|
|
415
420
|
public async getCollectionByCrn(crn: string): Promise<Collection> {
|
|
416
|
-
const { data } = await
|
|
421
|
+
const { data } = await CollectionService.getCollectionByCrn({
|
|
417
422
|
client: this.apiClient,
|
|
418
423
|
path: { crn },
|
|
419
424
|
});
|
|
@@ -497,7 +502,7 @@ export class ChromaClient {
|
|
|
497
502
|
schema,
|
|
498
503
|
});
|
|
499
504
|
|
|
500
|
-
const { data } = await
|
|
505
|
+
const { data } = await CollectionService.createCollection({
|
|
501
506
|
client: this.apiClient,
|
|
502
507
|
path: await this._path(),
|
|
503
508
|
body: {
|
|
@@ -544,7 +549,7 @@ export class ChromaClient {
|
|
|
544
549
|
* @param options.name - The name of the collection to delete
|
|
545
550
|
*/
|
|
546
551
|
public async deleteCollection({ name }: { name: string }): Promise<void> {
|
|
547
|
-
await
|
|
552
|
+
await CollectionService.deleteCollection({
|
|
548
553
|
client: this.apiClient,
|
|
549
554
|
path: { ...(await this._path()), collection_id: name },
|
|
550
555
|
});
|
|
@@ -556,7 +561,7 @@ export class ChromaClient {
|
|
|
556
561
|
* @warning This operation is irreversible and will delete all data
|
|
557
562
|
*/
|
|
558
563
|
public async reset(): Promise<void> {
|
|
559
|
-
await
|
|
564
|
+
await SystemService.reset({
|
|
560
565
|
client: this.apiClient,
|
|
561
566
|
});
|
|
562
567
|
}
|
|
@@ -566,7 +571,7 @@ export class ChromaClient {
|
|
|
566
571
|
* @returns Promise resolving to the server version string
|
|
567
572
|
*/
|
|
568
573
|
public async version(): Promise<string> {
|
|
569
|
-
const { data } = await
|
|
574
|
+
const { data } = await SystemService.version({
|
|
570
575
|
client: this.apiClient,
|
|
571
576
|
});
|
|
572
577
|
return data;
|
|
@@ -578,7 +583,7 @@ export class ChromaClient {
|
|
|
578
583
|
*/
|
|
579
584
|
public async getPreflightChecks(): Promise<ChecklistResponse> {
|
|
580
585
|
if (!this.preflightChecks) {
|
|
581
|
-
const { data } = await
|
|
586
|
+
const { data } = await SystemService.preFlightChecks({
|
|
582
587
|
client: this.apiClient,
|
|
583
588
|
});
|
|
584
589
|
this.preflightChecks = data;
|
package/src/collection.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
WhereDocument,
|
|
20
20
|
} from "./types";
|
|
21
21
|
import { Include, SparseVector, SearchPayload } from "./api";
|
|
22
|
-
import {
|
|
22
|
+
import { CollectionService, RecordService } from "./api";
|
|
23
23
|
import {
|
|
24
24
|
validateRecordSetLengthConsistency,
|
|
25
25
|
validateIDs,
|
|
@@ -747,7 +747,7 @@ export class CollectionImpl implements Collection {
|
|
|
747
747
|
}
|
|
748
748
|
|
|
749
749
|
public async count(): Promise<number> {
|
|
750
|
-
const { data } = await
|
|
750
|
+
const { data } = await RecordService.collectionCount({
|
|
751
751
|
client: this.apiClient,
|
|
752
752
|
path: await this.path(),
|
|
753
753
|
});
|
|
@@ -778,7 +778,7 @@ export class CollectionImpl implements Collection {
|
|
|
778
778
|
|
|
779
779
|
const preparedRecordSet = await this.prepareRecords({ recordSet });
|
|
780
780
|
|
|
781
|
-
await
|
|
781
|
+
await RecordService.collectionAdd({
|
|
782
782
|
client: this.apiClient,
|
|
783
783
|
path: await this.path(),
|
|
784
784
|
body: {
|
|
@@ -812,7 +812,7 @@ export class CollectionImpl implements Collection {
|
|
|
812
812
|
|
|
813
813
|
this.validateGet(include, ids, where, whereDocument);
|
|
814
814
|
|
|
815
|
-
const { data } = await
|
|
815
|
+
const { data } = await RecordService.collectionGet({
|
|
816
816
|
client: this.apiClient,
|
|
817
817
|
path: await this.path(),
|
|
818
818
|
body: {
|
|
@@ -875,7 +875,7 @@ export class CollectionImpl implements Collection {
|
|
|
875
875
|
nResults,
|
|
876
876
|
);
|
|
877
877
|
|
|
878
|
-
const { data } = await
|
|
878
|
+
const { data } = await RecordService.collectionQuery({
|
|
879
879
|
client: this.apiClient,
|
|
880
880
|
path: await this.path(),
|
|
881
881
|
body: {
|
|
@@ -923,7 +923,7 @@ export class CollectionImpl implements Collection {
|
|
|
923
923
|
}),
|
|
924
924
|
);
|
|
925
925
|
|
|
926
|
-
const { data } = await
|
|
926
|
+
const { data } = await RecordService.collectionSearch({
|
|
927
927
|
client: this.apiClient,
|
|
928
928
|
path: await this.path(),
|
|
929
929
|
body: {
|
|
@@ -953,12 +953,12 @@ export class CollectionImpl implements Collection {
|
|
|
953
953
|
|
|
954
954
|
const { updateConfiguration, updateEmbeddingFunction } = configuration
|
|
955
955
|
? await processUpdateCollectionConfig({
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
956
|
+
collectionName: this.name,
|
|
957
|
+
currentConfiguration: this.configuration,
|
|
958
|
+
newConfiguration: configuration,
|
|
959
|
+
currentEmbeddingFunction: this.embeddingFunction,
|
|
960
|
+
client: this.chromaClient,
|
|
961
|
+
})
|
|
962
962
|
: {};
|
|
963
963
|
|
|
964
964
|
if (updateEmbeddingFunction) {
|
|
@@ -973,7 +973,7 @@ export class CollectionImpl implements Collection {
|
|
|
973
973
|
};
|
|
974
974
|
}
|
|
975
975
|
|
|
976
|
-
await
|
|
976
|
+
await CollectionService.updateCollection({
|
|
977
977
|
client: this.apiClient,
|
|
978
978
|
path: await this.path(),
|
|
979
979
|
body: {
|
|
@@ -985,7 +985,7 @@ export class CollectionImpl implements Collection {
|
|
|
985
985
|
}
|
|
986
986
|
|
|
987
987
|
public async fork({ name }: { name: string }): Promise<Collection> {
|
|
988
|
-
const { data } = await
|
|
988
|
+
const { data } = await CollectionService.forkCollection({
|
|
989
989
|
client: this.apiClient,
|
|
990
990
|
path: await this.path(),
|
|
991
991
|
body: { new_name: name },
|
|
@@ -1030,7 +1030,7 @@ export class CollectionImpl implements Collection {
|
|
|
1030
1030
|
update: true,
|
|
1031
1031
|
});
|
|
1032
1032
|
|
|
1033
|
-
await
|
|
1033
|
+
await RecordService.collectionUpdate({
|
|
1034
1034
|
client: this.apiClient,
|
|
1035
1035
|
path: await this.path(),
|
|
1036
1036
|
body: {
|
|
@@ -1068,7 +1068,7 @@ export class CollectionImpl implements Collection {
|
|
|
1068
1068
|
recordSet,
|
|
1069
1069
|
});
|
|
1070
1070
|
|
|
1071
|
-
await
|
|
1071
|
+
await RecordService.collectionUpsert({
|
|
1072
1072
|
client: this.apiClient,
|
|
1073
1073
|
path: await this.path(),
|
|
1074
1074
|
body: {
|
|
@@ -1092,7 +1092,7 @@ export class CollectionImpl implements Collection {
|
|
|
1092
1092
|
}): Promise<void> {
|
|
1093
1093
|
this.validateDelete(ids, where, whereDocument);
|
|
1094
1094
|
|
|
1095
|
-
await
|
|
1095
|
+
await RecordService.collectionDelete({
|
|
1096
1096
|
client: this.apiClient,
|
|
1097
1097
|
path: await this.path(),
|
|
1098
1098
|
body: {
|
|
@@ -1104,7 +1104,7 @@ export class CollectionImpl implements Collection {
|
|
|
1104
1104
|
}
|
|
1105
1105
|
|
|
1106
1106
|
public async getIndexingStatus(): Promise<IndexingStatus> {
|
|
1107
|
-
const { data } = await
|
|
1107
|
+
const { data } = await RecordService.indexingStatus({
|
|
1108
1108
|
client: this.apiClient,
|
|
1109
1109
|
path: await this.path(),
|
|
1110
1110
|
});
|
|
@@ -46,16 +46,37 @@ export class Key {
|
|
|
46
46
|
return createComparisonWhere(this.name, "$nin", array);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Contains filter.
|
|
51
|
+
*
|
|
52
|
+
* On `Key.DOCUMENT`: substring search (value must be a string).
|
|
53
|
+
* On metadata fields: checks if the array field contains the scalar value.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* K.DOCUMENT.contains("machine learning") // document substring
|
|
57
|
+
* K("tags").contains("action") // metadata array contains
|
|
58
|
+
* K("scores").contains(42) // metadata array contains
|
|
59
|
+
*/
|
|
60
|
+
public contains(value: string | number | boolean): WhereExpression {
|
|
61
|
+
if (this.name === "#document" && typeof value !== "string") {
|
|
62
|
+
throw new TypeError("K.DOCUMENT.contains requires a string value");
|
|
52
63
|
}
|
|
53
64
|
return createComparisonWhere(this.name, "$contains", value);
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Not-contains filter.
|
|
69
|
+
*
|
|
70
|
+
* On `Key.DOCUMENT`: excludes documents containing the substring.
|
|
71
|
+
* On metadata fields: checks that the array field does not contain the scalar value.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* K.DOCUMENT.notContains("deprecated") // document substring exclusion
|
|
75
|
+
* K("tags").notContains("draft") // metadata array not-contains
|
|
76
|
+
*/
|
|
77
|
+
public notContains(value: string | number | boolean): WhereExpression {
|
|
78
|
+
if (this.name === "#document" && typeof value !== "string") {
|
|
79
|
+
throw new TypeError("K.DOCUMENT.notContains requires a string value");
|
|
59
80
|
}
|
|
60
81
|
return createComparisonWhere(this.name, "$not_contains", value);
|
|
61
82
|
}
|
package/src/types.ts
CHANGED
|
@@ -30,22 +30,41 @@ export type ReadLevel = (typeof ReadLevel)[keyof typeof ReadLevel];
|
|
|
30
30
|
*/
|
|
31
31
|
export type { SparseVector };
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Scalar metadata values that can be stored in arrays.
|
|
35
|
+
*/
|
|
36
|
+
export type MetadataScalar = boolean | number | string;
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Metadata that can be associated with a collection.
|
|
35
|
-
* Values
|
|
40
|
+
* Values can be boolean, number, string, SparseVector, typed arrays, or null.
|
|
36
41
|
*/
|
|
37
42
|
export type CollectionMetadata = Record<
|
|
38
43
|
string,
|
|
39
|
-
|
|
44
|
+
| boolean
|
|
45
|
+
| number
|
|
46
|
+
| string
|
|
47
|
+
| SparseVector
|
|
48
|
+
| boolean[]
|
|
49
|
+
| number[]
|
|
50
|
+
| string[]
|
|
51
|
+
| null
|
|
40
52
|
>;
|
|
41
53
|
|
|
42
54
|
/**
|
|
43
55
|
* Metadata that can be associated with individual records.
|
|
44
|
-
* Values
|
|
56
|
+
* Values can be boolean, number, string, SparseVector, typed arrays, or null.
|
|
45
57
|
*/
|
|
46
58
|
export type Metadata = Record<
|
|
47
59
|
string,
|
|
48
|
-
|
|
60
|
+
| boolean
|
|
61
|
+
| number
|
|
62
|
+
| string
|
|
63
|
+
| SparseVector
|
|
64
|
+
| boolean[]
|
|
65
|
+
| number[]
|
|
66
|
+
| string[]
|
|
67
|
+
| null
|
|
49
68
|
>;
|
|
50
69
|
|
|
51
70
|
/**
|
|
@@ -106,6 +125,8 @@ type WhereOperator = "$gt" | "$gte" | "$lt" | "$lte" | "$ne" | "$eq";
|
|
|
106
125
|
|
|
107
126
|
type InclusionExclusionOperator = "$in" | "$nin";
|
|
108
127
|
|
|
128
|
+
type ArrayContainsOperator = "$contains" | "$not_contains";
|
|
129
|
+
|
|
109
130
|
type OperatorExpression =
|
|
110
131
|
| { $gt: LiteralValue }
|
|
111
132
|
| { $gte: LiteralValue }
|
|
@@ -116,7 +137,9 @@ type OperatorExpression =
|
|
|
116
137
|
| { $and: LiteralValue }
|
|
117
138
|
| { $or: LiteralValue }
|
|
118
139
|
| { $in: LiteralValue[] }
|
|
119
|
-
| { $nin: LiteralValue[] }
|
|
140
|
+
| { $nin: LiteralValue[] }
|
|
141
|
+
| { $contains: LiteralValue }
|
|
142
|
+
| { $not_contains: LiteralValue };
|
|
120
143
|
|
|
121
144
|
/**
|
|
122
145
|
* Where clause for filtering records based on metadata.
|
package/src/utils.ts
CHANGED
|
@@ -260,19 +260,50 @@ export const validateMetadata = (metadata?: Metadata) => {
|
|
|
260
260
|
throw new ChromaValueError("Expected metadata to be non-empty");
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
263
|
+
const validateMetadataListValue = (key: string, v: unknown[]): void => {
|
|
264
|
+
if (v.length === 0) {
|
|
265
|
+
throw new ChromaValueError(
|
|
266
|
+
`Expected metadata list value for key '${key}' to be non-empty`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const firstType = typeof v[0];
|
|
270
|
+
for (const item of v) {
|
|
271
|
+
if (
|
|
272
|
+
typeof item !== "string" &&
|
|
273
|
+
typeof item !== "number" &&
|
|
274
|
+
typeof item !== "boolean"
|
|
275
|
+
) {
|
|
276
|
+
throw new ChromaValueError(
|
|
277
|
+
`Expected metadata list value for key '${key}' to contain only strings, numbers, or booleans, got ${typeof item}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (typeof item !== firstType) {
|
|
281
|
+
throw new ChromaValueError(
|
|
282
|
+
`Expected metadata list value for key '${key}' to contain only the same type, got mixed types`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
for (const [key, v] of Object.entries(metadata)) {
|
|
289
|
+
if (
|
|
290
|
+
v === null ||
|
|
291
|
+
v === undefined ||
|
|
292
|
+
typeof v === "string" ||
|
|
293
|
+
typeof v === "number" ||
|
|
294
|
+
typeof v === "boolean"
|
|
295
|
+
) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (validateSparseVector(v)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(v)) {
|
|
302
|
+
validateMetadataListValue(key, v);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
274
305
|
throw new ChromaValueError(
|
|
275
|
-
|
|
306
|
+
`Expected metadata value for key '${key}' to be a string, number, boolean, SparseVector, typed array (string[], number[], boolean[]), or null`,
|
|
276
307
|
);
|
|
277
308
|
}
|
|
278
309
|
};
|
|
@@ -289,6 +320,9 @@ type SerializedMetadataValue =
|
|
|
289
320
|
| string
|
|
290
321
|
| SerializedSparseVector
|
|
291
322
|
| SparseVector
|
|
323
|
+
| Array<boolean>
|
|
324
|
+
| Array<number>
|
|
325
|
+
| Array<string>
|
|
292
326
|
| null;
|
|
293
327
|
|
|
294
328
|
export type SerializedMetadata = Record<string, SerializedMetadataValue>;
|
|
@@ -315,10 +349,13 @@ export const serializeMetadata = (
|
|
|
315
349
|
const result: SerializedMetadata = {};
|
|
316
350
|
|
|
317
351
|
Object.entries(metadata).forEach(([key, value]) => {
|
|
352
|
+
if (value === null || value === undefined) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
318
355
|
if (validateSparseVector(value)) {
|
|
319
356
|
result[key] = toSerializedSparseVector(value);
|
|
320
357
|
} else {
|
|
321
|
-
result[key] = value
|
|
358
|
+
result[key] = value;
|
|
322
359
|
}
|
|
323
360
|
});
|
|
324
361
|
|
|
@@ -555,12 +592,30 @@ export const validateWhere = (where: Where) => {
|
|
|
555
592
|
}
|
|
556
593
|
|
|
557
594
|
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
595
|
+
["$contains", "$not_contains"].includes(operator) &&
|
|
596
|
+
!["string", "number", "boolean"].includes(typeof operand)
|
|
597
|
+
) {
|
|
598
|
+
throw new ChromaValueError(
|
|
599
|
+
`Expected operand value to be a string, number, or boolean for ${operator}, but got ${typeof operand}`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (
|
|
604
|
+
![
|
|
605
|
+
"$gt",
|
|
606
|
+
"$gte",
|
|
607
|
+
"$lt",
|
|
608
|
+
"$lte",
|
|
609
|
+
"$ne",
|
|
610
|
+
"$eq",
|
|
611
|
+
"$in",
|
|
612
|
+
"$nin",
|
|
613
|
+
"$contains",
|
|
614
|
+
"$not_contains",
|
|
615
|
+
].includes(operator)
|
|
561
616
|
) {
|
|
562
617
|
throw new ChromaValueError(
|
|
563
|
-
`Expected operator to be one of $gt, $gte, $lt, $lte, $ne, $eq, $in, $nin, but got ${operator}`,
|
|
618
|
+
`Expected operator to be one of $gt, $gte, $lt, $lte, $ne, $eq, $in, $nin, $contains, $not_contains, but got ${operator}`,
|
|
564
619
|
);
|
|
565
620
|
}
|
|
566
621
|
|