Skip to content

Commit 3b28c30

Browse files
authored
Clients: add modify and clear (#23)
* add modify and clear * test blyss service via python client
1 parent 1f5c056 commit 3b28c30

File tree

16 files changed

+377
-105
lines changed

16 files changed

+377
-105
lines changed

.github/workflows/build-python.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Build Python SDK
22

3+
env:
4+
BLYSS_STAGING_SERVER: https://dev2.api.blyss.dev
5+
BLYSS_STAGING_API_KEY: Gh1pz1kEiNa1npEdDaRRvM1LsVypM1u2x1YbGb54
6+
37
on:
48
push:
59
branches: [ "main" ]
@@ -26,6 +30,14 @@ jobs:
2630
- uses: actions/setup-python@v4
2731
with:
2832
python-version: '3.10'
33+
- name: Install Python SDK
34+
working-directory: python
35+
shell: bash
36+
run: pip install .
37+
- name: Test Python SDK
38+
working-directory: python
39+
shell: bash
40+
run: python tests/test_service.py
2941
- name: Build wheels
3042
uses: PyO3/maturin-action@v1
3143
with:

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"python.analysis.typeCheckingMode": "basic"
2+
"python.analysis.typeCheckingMode": "basic",
3+
"editor.formatOnSave": true
34
}

e2e-tests/api.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { Bucket, Client } from '@blyss/sdk';
2+
const blyss = require('@blyss/sdk/node');
3+
4+
async function keyToValue(key: string, len: number): Promise<Uint8Array> {
5+
const keyBytes = new TextEncoder().encode(key);
6+
const value = new Uint8Array(len);
7+
let i = 0;
8+
// fill the value with the hash.
9+
// if the hash is smaller than the value, we hash the hash again.
10+
while (i < len) {
11+
const hash = await crypto.subtle.digest('SHA-1', keyBytes);
12+
const hashBytes = new Uint8Array(hash);
13+
const toCopy = Math.min(hashBytes.length, len - i);
14+
value.set(hashBytes.slice(0, toCopy), i);
15+
i += toCopy;
16+
}
17+
return value;
18+
}
19+
20+
async function verifyRead(key: string, value: Uint8Array): Promise<void> {
21+
const expected = await keyToValue(key, value.length);
22+
if (expected.toString() !== value.toString()) {
23+
throw new Error('Incorrect value for key ' + key);
24+
}
25+
}
26+
27+
function generateKeys(n: number, seed: number = 0): string[] {
28+
return new Array(n).fill(0).map(
29+
(_, i) => seed.toString() + '-' + i.toString()
30+
);
31+
}
32+
33+
function generateBucketName(): string {
34+
return 'api-tester-' + Math.random().toString(16).substring(2, 10);
35+
}
36+
37+
async function testBlyssService(endpoint: string = 'https://dev2.api.blyss.dev') {
38+
const apiKey = process.env.BLYSS_API_KEY;
39+
if (!apiKey) {
40+
throw new Error('BLYSS_API_KEY environment variable is not set');
41+
}
42+
console.log('Using key: ' + apiKey + ' to connect to ' + endpoint);
43+
const client: Client = await new blyss.Client(
44+
{
45+
endpoint: endpoint,
46+
apiKey: apiKey
47+
}
48+
);
49+
// generate random string for bucket name
50+
const bucketName = generateBucketName();
51+
await client.create(bucketName);
52+
const bucket: Bucket = await client.connect(bucketName);
53+
console.log(bucket.metadata);
54+
55+
// generate N random keys
56+
const N = 100;
57+
const itemSize = 32;
58+
let localKeys = generateKeys(N);
59+
function getRandomKey(): string {
60+
return localKeys[Math.floor(Math.random() * localKeys.length)];
61+
}
62+
// write all N keys
63+
await bucket.write(
64+
await Promise.all(localKeys.map(
65+
async (k) => ({
66+
k: await keyToValue(k, itemSize)
67+
})
68+
))
69+
);
70+
console.log(`Wrote ${N} keys`);
71+
72+
// read a random key
73+
let testKey = getRandomKey();
74+
let value = await bucket.privateRead(testKey);
75+
await verifyRead(testKey, value);
76+
console.log(`Read key ${testKey}`);
77+
78+
// delete testKey from the bucket, and localData.
79+
await bucket.deleteKey(testKey);
80+
localKeys.splice(localKeys.indexOf(testKey), 1);
81+
console.log(`Deleted key ${testKey}`);
82+
83+
// write a new value
84+
testKey = 'newKey0';
85+
await bucket.write({ testKey: keyToValue(testKey, itemSize) });
86+
localKeys.push(testKey);
87+
console.log(`Wrote key ${testKey}`);
88+
89+
// clear all keys
90+
await bucket.clearEntireBucket();
91+
localKeys = [];
92+
console.log('Cleared bucket');
93+
94+
// write a new set of N keys
95+
localKeys = generateKeys(N, 1);
96+
await bucket.write(
97+
await Promise.all(localKeys.map(
98+
async (k) => ({
99+
k: await keyToValue(k, itemSize)
100+
})
101+
))
102+
);
103+
console.log(`Wrote ${N} keys`);
104+
105+
// rename the bucket
106+
const newBucketName = bucketName + '-rn';
107+
await bucket.rename(newBucketName);
108+
console.log(`Renamed bucket`);
109+
console.log(await bucket.info());
110+
111+
// random read
112+
testKey = getRandomKey();
113+
value = await bucket.privateRead(testKey);
114+
await verifyRead(testKey, value);
115+
console.log(`Read key ${testKey}`);
116+
117+
// destroy the bucket
118+
await bucket.destroyEntireBucket();
119+
console.log(`Destroyed bucket ${bucket.name}`);
120+
}
121+
122+
async function main() {
123+
const endpoint = "https://dev2.api.blyss.dev"
124+
console.log('Testing Blyss service at URL ' + endpoint);
125+
await testBlyssService(endpoint);
126+
console.log('All tests completed successfully.');
127+
}
128+
129+
main();

js/bucket/bucket.ts

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { base64ToBytes, getRandomSeed } from '../client/seed';
33
import { decompress } from '../compression/bz2_decompress';
44
import { bloomLookup } from '../data/bloom';
55
import {
6-
DataWithMetadata,
76
concatBytes,
87
deserialize,
98
deserializeChunks,
@@ -40,7 +39,7 @@ export class Bucket {
4039
readonly api: Api;
4140

4241
/** The name of this bucket. */
43-
readonly name: string;
42+
name: string;
4443

4544
/**
4645
* The secret seed for this instance of the client, which can be saved and
@@ -110,7 +109,7 @@ export class Bucket {
110109
try {
111110
decompressedResult = decompress(decryptedResult);
112111
} catch (e) {
113-
console.error('decompress error', e);
112+
console.error(`key ${key} not found (decompression failed)`);
114113
}
115114
if (decompressedResult === null) {
116115
return null;
@@ -120,7 +119,7 @@ export class Bucket {
120119
try {
121120
extractedResult = this.lib.extractResult(key, decompressedResult);
122121
} catch (e) {
123-
console.error('extraction error', e);
122+
console.error(`key ${key} not found (extraction failed)`);
124123
}
125124
if (extractedResult === null) {
126125
return null;
@@ -151,7 +150,7 @@ export class Bucket {
151150

152151
private async performPrivateReads(
153152
keys: string[]
154-
): Promise<DataWithMetadata[]> {
153+
): Promise<any[]> {
155154
if (!this.uuid || !this.check(this.uuid)) {
156155
await this.setup();
157156
}
@@ -185,7 +184,7 @@ export class Bucket {
185184
return endResults;
186185
}
187186

188-
private async performPrivateRead(key: string): Promise<DataWithMetadata> {
187+
private async performPrivateRead(key: string): Promise<any> {
189188
return (await this.performPrivateReads([key]))[0];
190189
}
191190

@@ -320,6 +319,15 @@ export class Bucket {
320319
return await this.api.meta(this.name);
321320
}
322321

322+
/** Renames this bucket, leaving all data and other bucket settings intact. */
323+
async rename(newBucketName: string): Promise<BucketMetadata> {
324+
const bucketCreateReq = {
325+
name: newBucketName
326+
};
327+
await this.api.modify(this.name, JSON.stringify(bucketCreateReq));
328+
this.name = newBucketName;
329+
}
330+
323331
/** Gets info on all keys in this bucket. */
324332
async listKeys(): Promise<KeyInfo[]> {
325333
this.ensureSpiral();
@@ -333,32 +341,28 @@ export class Bucket {
333341
* key-value pairs to write. Keys must be strings, and values may be any
334342
* JSON-serializable value or a Uint8Array. The maximum size of a key is
335343
* 1024 UTF-8 bytes.
336-
* @param {{ [key: string]: any }} [metadata] - An optional object containing
337-
* metadata. Each key of this object should also be a key of
338-
* `keyValuePairs`, and the value should be some metadata object to store
339-
* with the values being written.
340344
*/
341345
async write(
342-
keyValuePairs: { [key: string]: any },
343-
metadata?: { [key: string]: any }
346+
keyValuePairs: { [key: string]: any }
344347
) {
345348
this.ensureSpiral();
346349

347350
const data = [];
348351
for (const key in keyValuePairs) {
349352
if (Object.prototype.hasOwnProperty.call(keyValuePairs, key)) {
350353
const value = keyValuePairs[key];
351-
let valueMetadata = undefined;
352-
if (metadata && Object.prototype.hasOwnProperty.call(metadata, key)) {
353-
valueMetadata = metadata[key];
354-
}
355-
const valueBytes = serialize(value, valueMetadata);
354+
const valueBytes = serialize(value);
356355
const keyBytes = new TextEncoder().encode(key);
357356
const serializedKeyValue = wrapKeyValue(keyBytes, valueBytes);
358357
data.push(serializedKeyValue);
358+
// const kv = {
359+
// key: key,
360+
// value: Buffer.from(valueBytes).toString('base64')
361+
// }
359362
}
360363
}
361364
const concatenatedData = concatBytes(data);
365+
// const concatenatedData = serialize(data);
362366
await this.api.write(this.name, concatenatedData);
363367
}
364368

@@ -385,6 +389,14 @@ export class Bucket {
385389
await this.api.destroy(this.name);
386390
}
387391

392+
/**
393+
* Clears the contents of the entire bucket, and all data inside of it. This action is
394+
* permanent and irreversible.
395+
*/
396+
async clearEntireBucket() {
397+
await this.api.clear(this.name);
398+
}
399+
388400
/**
389401
* Privately reads the supplied key from the bucket, returning the value
390402
* corresponding to the key.
@@ -398,28 +410,13 @@ export class Bucket {
398410
this.ensureSpiral();
399411

400412
if (Array.isArray(key)) {
401-
return (await this.performPrivateReads(key)).map(r => r.data);
413+
return (await this.performPrivateReads(key));
402414
} else {
403415
const result = await this.performPrivateRead(key);
404-
return result ? result.data : null;
416+
return result ? result : null;
405417
}
406418
}
407419

408-
/**
409-
* Privately reads the supplied key from the bucket, returning the value and
410-
* metadata corresponding to the key.
411-
*
412-
* No entity, including the Blyss service, should be able to determine which
413-
* key this method was called for.
414-
*
415-
* @param {string} key - The key to _privately_ retrieve the value of.
416-
*/
417-
async privateReadWithMetadata(key: string): Promise<DataWithMetadata> {
418-
this.ensureSpiral();
419-
420-
return await this.performPrivateRead(key);
421-
}
422-
423420
/**
424421
* Privately intersects the given set of keys with the keys in this bucket,
425422
* returning the keys that intersected and their values. This is generally
@@ -437,7 +434,7 @@ export class Bucket {
437434
this.ensureSpiral();
438435

439436
if (keys.length < BLOOM_CUTOFF) {
440-
return (await this.performPrivateReads(keys)).map(x => x.data);
437+
return (await this.performPrivateReads(keys));
441438
}
442439

443440
const bloomFilter = await this.api.bloom(this.name);
@@ -451,7 +448,7 @@ export class Bucket {
451448
if (!retrieveValues) {
452449
return matches;
453450
}
454-
return (await this.performPrivateReads(matches)).map(x => x.data);
451+
return (await this.performPrivateReads(matches));
455452
}
456453

457454
/**

js/client/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { gzip } from '../compression/pako';
44
import { BloomFilter, bloomFilterFromBytes } from '../data/bloom';
55

66
const CREATE_PATH = '/create';
7+
const MODIFY_PATH = '/modify';
8+
const CLEAR_PATH = '/clear';
79
const DESTROY_PATH = '/destroy';
810
const CHECK_PATH = '/check';
911
const DELETE_PATH = '/delete';
@@ -215,6 +217,18 @@ class Api {
215217
return await getData(this.apiKey, this.urlFor(bucketName, META_PATH), true);
216218
}
217219

220+
/**
221+
* Modify a bucket's properties.
222+
*
223+
* @param bucketName The name of the bucket.
224+
* @param dataJson A JSON-encoded string of the bucket metadata. Supports the same fields as `create()`.
225+
* @returns Bucket metadata after update.
226+
*/
227+
async modify(bucketName: string, dataJson: string): Promise<BucketMetadata> {
228+
return await postData(this.apiKey, this.urlFor(bucketName, MODIFY_PATH), dataJson, true);
229+
}
230+
231+
218232
/**
219233
* Get the Bloom filter for keys in this bucket. The Bloom filter contains all
220234
* keys ever inserted into this bucket; it does not remove deleted keys.
@@ -304,6 +318,16 @@ class Api {
304318
);
305319
}
306320

321+
/** Clear contents of this bucket. */
322+
async clear(bucketName: string) {
323+
await postData(
324+
this.apiKey,
325+
this.urlFor(bucketName, CLEAR_PATH),
326+
'',
327+
false
328+
);
329+
}
330+
307331
/** Write to this bucket. */
308332
async write(bucketName: string, data: Uint8Array) {
309333
await postData(

0 commit comments

Comments
 (0)