mirror of
https://github.com/blyssprivacy/sdk.git
synced 2026-01-09 15:18:01 -05:00
Clients: add modify and clear (#23)
* add modify and clear * test blyss service via python client
This commit is contained in:
12
.github/workflows/build-python.yml
vendored
12
.github/workflows/build-python.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Build Python SDK
|
||||
|
||||
env:
|
||||
BLYSS_STAGING_SERVER: https://dev2.api.blyss.dev
|
||||
BLYSS_STAGING_API_KEY: Gh1pz1kEiNa1npEdDaRRvM1LsVypM1u2x1YbGb54
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
@@ -26,6 +30,14 @@ jobs:
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install Python SDK
|
||||
working-directory: python
|
||||
shell: bash
|
||||
run: pip install .
|
||||
- name: Test Python SDK
|
||||
working-directory: python
|
||||
shell: bash
|
||||
run: python tests/test_service.py
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
129
e2e-tests/api.ts
Normal file
129
e2e-tests/api.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Bucket, Client } from '@blyss/sdk';
|
||||
const blyss = require('@blyss/sdk/node');
|
||||
|
||||
async function keyToValue(key: string, len: number): Promise<Uint8Array> {
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const value = new Uint8Array(len);
|
||||
let i = 0;
|
||||
// fill the value with the hash.
|
||||
// if the hash is smaller than the value, we hash the hash again.
|
||||
while (i < len) {
|
||||
const hash = await crypto.subtle.digest('SHA-1', keyBytes);
|
||||
const hashBytes = new Uint8Array(hash);
|
||||
const toCopy = Math.min(hashBytes.length, len - i);
|
||||
value.set(hashBytes.slice(0, toCopy), i);
|
||||
i += toCopy;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function verifyRead(key: string, value: Uint8Array): Promise<void> {
|
||||
const expected = await keyToValue(key, value.length);
|
||||
if (expected.toString() !== value.toString()) {
|
||||
throw new Error('Incorrect value for key ' + key);
|
||||
}
|
||||
}
|
||||
|
||||
function generateKeys(n: number, seed: number = 0): string[] {
|
||||
return new Array(n).fill(0).map(
|
||||
(_, i) => seed.toString() + '-' + i.toString()
|
||||
);
|
||||
}
|
||||
|
||||
function generateBucketName(): string {
|
||||
return 'api-tester-' + Math.random().toString(16).substring(2, 10);
|
||||
}
|
||||
|
||||
async function testBlyssService(endpoint: string = 'https://dev2.api.blyss.dev') {
|
||||
const apiKey = process.env.BLYSS_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('BLYSS_API_KEY environment variable is not set');
|
||||
}
|
||||
console.log('Using key: ' + apiKey + ' to connect to ' + endpoint);
|
||||
const client: Client = await new blyss.Client(
|
||||
{
|
||||
endpoint: endpoint,
|
||||
apiKey: apiKey
|
||||
}
|
||||
);
|
||||
// generate random string for bucket name
|
||||
const bucketName = generateBucketName();
|
||||
await client.create(bucketName);
|
||||
const bucket: Bucket = await client.connect(bucketName);
|
||||
console.log(bucket.metadata);
|
||||
|
||||
// generate N random keys
|
||||
const N = 100;
|
||||
const itemSize = 32;
|
||||
let localKeys = generateKeys(N);
|
||||
function getRandomKey(): string {
|
||||
return localKeys[Math.floor(Math.random() * localKeys.length)];
|
||||
}
|
||||
// write all N keys
|
||||
await bucket.write(
|
||||
await Promise.all(localKeys.map(
|
||||
async (k) => ({
|
||||
k: await keyToValue(k, itemSize)
|
||||
})
|
||||
))
|
||||
);
|
||||
console.log(`Wrote ${N} keys`);
|
||||
|
||||
// read a random key
|
||||
let testKey = getRandomKey();
|
||||
let value = await bucket.privateRead(testKey);
|
||||
await verifyRead(testKey, value);
|
||||
console.log(`Read key ${testKey}`);
|
||||
|
||||
// delete testKey from the bucket, and localData.
|
||||
await bucket.deleteKey(testKey);
|
||||
localKeys.splice(localKeys.indexOf(testKey), 1);
|
||||
console.log(`Deleted key ${testKey}`);
|
||||
|
||||
// write a new value
|
||||
testKey = 'newKey0';
|
||||
await bucket.write({ testKey: keyToValue(testKey, itemSize) });
|
||||
localKeys.push(testKey);
|
||||
console.log(`Wrote key ${testKey}`);
|
||||
|
||||
// clear all keys
|
||||
await bucket.clearEntireBucket();
|
||||
localKeys = [];
|
||||
console.log('Cleared bucket');
|
||||
|
||||
// write a new set of N keys
|
||||
localKeys = generateKeys(N, 1);
|
||||
await bucket.write(
|
||||
await Promise.all(localKeys.map(
|
||||
async (k) => ({
|
||||
k: await keyToValue(k, itemSize)
|
||||
})
|
||||
))
|
||||
);
|
||||
console.log(`Wrote ${N} keys`);
|
||||
|
||||
// rename the bucket
|
||||
const newBucketName = bucketName + '-rn';
|
||||
await bucket.rename(newBucketName);
|
||||
console.log(`Renamed bucket`);
|
||||
console.log(await bucket.info());
|
||||
|
||||
// random read
|
||||
testKey = getRandomKey();
|
||||
value = await bucket.privateRead(testKey);
|
||||
await verifyRead(testKey, value);
|
||||
console.log(`Read key ${testKey}`);
|
||||
|
||||
// destroy the bucket
|
||||
await bucket.destroyEntireBucket();
|
||||
console.log(`Destroyed bucket ${bucket.name}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const endpoint = "https://dev2.api.blyss.dev"
|
||||
console.log('Testing Blyss service at URL ' + endpoint);
|
||||
await testBlyssService(endpoint);
|
||||
console.log('All tests completed successfully.');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -3,7 +3,6 @@ import { base64ToBytes, getRandomSeed } from '../client/seed';
|
||||
import { decompress } from '../compression/bz2_decompress';
|
||||
import { bloomLookup } from '../data/bloom';
|
||||
import {
|
||||
DataWithMetadata,
|
||||
concatBytes,
|
||||
deserialize,
|
||||
deserializeChunks,
|
||||
@@ -40,7 +39,7 @@ export class Bucket {
|
||||
readonly api: Api;
|
||||
|
||||
/** The name of this bucket. */
|
||||
readonly name: string;
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The secret seed for this instance of the client, which can be saved and
|
||||
@@ -110,7 +109,7 @@ export class Bucket {
|
||||
try {
|
||||
decompressedResult = decompress(decryptedResult);
|
||||
} catch (e) {
|
||||
console.error('decompress error', e);
|
||||
console.error(`key ${key} not found (decompression failed)`);
|
||||
}
|
||||
if (decompressedResult === null) {
|
||||
return null;
|
||||
@@ -120,7 +119,7 @@ export class Bucket {
|
||||
try {
|
||||
extractedResult = this.lib.extractResult(key, decompressedResult);
|
||||
} catch (e) {
|
||||
console.error('extraction error', e);
|
||||
console.error(`key ${key} not found (extraction failed)`);
|
||||
}
|
||||
if (extractedResult === null) {
|
||||
return null;
|
||||
@@ -151,7 +150,7 @@ export class Bucket {
|
||||
|
||||
private async performPrivateReads(
|
||||
keys: string[]
|
||||
): Promise<DataWithMetadata[]> {
|
||||
): Promise<any[]> {
|
||||
if (!this.uuid || !this.check(this.uuid)) {
|
||||
await this.setup();
|
||||
}
|
||||
@@ -185,7 +184,7 @@ export class Bucket {
|
||||
return endResults;
|
||||
}
|
||||
|
||||
private async performPrivateRead(key: string): Promise<DataWithMetadata> {
|
||||
private async performPrivateRead(key: string): Promise<any> {
|
||||
return (await this.performPrivateReads([key]))[0];
|
||||
}
|
||||
|
||||
@@ -320,6 +319,15 @@ export class Bucket {
|
||||
return await this.api.meta(this.name);
|
||||
}
|
||||
|
||||
/** Renames this bucket, leaving all data and other bucket settings intact. */
|
||||
async rename(newBucketName: string): Promise<BucketMetadata> {
|
||||
const bucketCreateReq = {
|
||||
name: newBucketName
|
||||
};
|
||||
await this.api.modify(this.name, JSON.stringify(bucketCreateReq));
|
||||
this.name = newBucketName;
|
||||
}
|
||||
|
||||
/** Gets info on all keys in this bucket. */
|
||||
async listKeys(): Promise<KeyInfo[]> {
|
||||
this.ensureSpiral();
|
||||
@@ -333,14 +341,9 @@ export class Bucket {
|
||||
* key-value pairs to write. Keys must be strings, and values may be any
|
||||
* JSON-serializable value or a Uint8Array. The maximum size of a key is
|
||||
* 1024 UTF-8 bytes.
|
||||
* @param {{ [key: string]: any }} [metadata] - An optional object containing
|
||||
* metadata. Each key of this object should also be a key of
|
||||
* `keyValuePairs`, and the value should be some metadata object to store
|
||||
* with the values being written.
|
||||
*/
|
||||
async write(
|
||||
keyValuePairs: { [key: string]: any },
|
||||
metadata?: { [key: string]: any }
|
||||
keyValuePairs: { [key: string]: any }
|
||||
) {
|
||||
this.ensureSpiral();
|
||||
|
||||
@@ -348,17 +351,18 @@ export class Bucket {
|
||||
for (const key in keyValuePairs) {
|
||||
if (Object.prototype.hasOwnProperty.call(keyValuePairs, key)) {
|
||||
const value = keyValuePairs[key];
|
||||
let valueMetadata = undefined;
|
||||
if (metadata && Object.prototype.hasOwnProperty.call(metadata, key)) {
|
||||
valueMetadata = metadata[key];
|
||||
}
|
||||
const valueBytes = serialize(value, valueMetadata);
|
||||
const valueBytes = serialize(value);
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const serializedKeyValue = wrapKeyValue(keyBytes, valueBytes);
|
||||
data.push(serializedKeyValue);
|
||||
// const kv = {
|
||||
// key: key,
|
||||
// value: Buffer.from(valueBytes).toString('base64')
|
||||
// }
|
||||
}
|
||||
}
|
||||
const concatenatedData = concatBytes(data);
|
||||
// const concatenatedData = serialize(data);
|
||||
await this.api.write(this.name, concatenatedData);
|
||||
}
|
||||
|
||||
@@ -385,6 +389,14 @@ export class Bucket {
|
||||
await this.api.destroy(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the contents of the entire bucket, and all data inside of it. This action is
|
||||
* permanent and irreversible.
|
||||
*/
|
||||
async clearEntireBucket() {
|
||||
await this.api.clear(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Privately reads the supplied key from the bucket, returning the value
|
||||
* corresponding to the key.
|
||||
@@ -398,28 +410,13 @@ export class Bucket {
|
||||
this.ensureSpiral();
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
return (await this.performPrivateReads(key)).map(r => r.data);
|
||||
return (await this.performPrivateReads(key));
|
||||
} else {
|
||||
const result = await this.performPrivateRead(key);
|
||||
return result ? result.data : null;
|
||||
return result ? result : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Privately reads the supplied key from the bucket, returning the value and
|
||||
* metadata corresponding to the key.
|
||||
*
|
||||
* No entity, including the Blyss service, should be able to determine which
|
||||
* key this method was called for.
|
||||
*
|
||||
* @param {string} key - The key to _privately_ retrieve the value of.
|
||||
*/
|
||||
async privateReadWithMetadata(key: string): Promise<DataWithMetadata> {
|
||||
this.ensureSpiral();
|
||||
|
||||
return await this.performPrivateRead(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Privately intersects the given set of keys with the keys in this bucket,
|
||||
* returning the keys that intersected and their values. This is generally
|
||||
@@ -437,7 +434,7 @@ export class Bucket {
|
||||
this.ensureSpiral();
|
||||
|
||||
if (keys.length < BLOOM_CUTOFF) {
|
||||
return (await this.performPrivateReads(keys)).map(x => x.data);
|
||||
return (await this.performPrivateReads(keys));
|
||||
}
|
||||
|
||||
const bloomFilter = await this.api.bloom(this.name);
|
||||
@@ -451,7 +448,7 @@ export class Bucket {
|
||||
if (!retrieveValues) {
|
||||
return matches;
|
||||
}
|
||||
return (await this.performPrivateReads(matches)).map(x => x.data);
|
||||
return (await this.performPrivateReads(matches));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,8 @@ import { gzip } from '../compression/pako';
|
||||
import { BloomFilter, bloomFilterFromBytes } from '../data/bloom';
|
||||
|
||||
const CREATE_PATH = '/create';
|
||||
const MODIFY_PATH = '/modify';
|
||||
const CLEAR_PATH = '/clear';
|
||||
const DESTROY_PATH = '/destroy';
|
||||
const CHECK_PATH = '/check';
|
||||
const DELETE_PATH = '/delete';
|
||||
@@ -215,6 +217,18 @@ class Api {
|
||||
return await getData(this.apiKey, this.urlFor(bucketName, META_PATH), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a bucket's properties.
|
||||
*
|
||||
* @param bucketName The name of the bucket.
|
||||
* @param dataJson A JSON-encoded string of the bucket metadata. Supports the same fields as `create()`.
|
||||
* @returns Bucket metadata after update.
|
||||
*/
|
||||
async modify(bucketName: string, dataJson: string): Promise<BucketMetadata> {
|
||||
return await postData(this.apiKey, this.urlFor(bucketName, MODIFY_PATH), dataJson, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Bloom filter for keys in this bucket. The Bloom filter contains all
|
||||
* keys ever inserted into this bucket; it does not remove deleted keys.
|
||||
@@ -304,6 +318,16 @@ class Api {
|
||||
);
|
||||
}
|
||||
|
||||
/** Clear contents of this bucket. */
|
||||
async clear(bucketName: string) {
|
||||
await postData(
|
||||
this.apiKey,
|
||||
this.urlFor(bucketName, CLEAR_PATH),
|
||||
'',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/** Write to this bucket. */
|
||||
async write(bucketName: string, data: Uint8Array) {
|
||||
await postData(
|
||||
|
||||
@@ -48,7 +48,12 @@ export function mergeUint8Arrays(
|
||||
return mergedArray;
|
||||
}
|
||||
|
||||
function getObjectAsBytes(obj: any): Uint8Array {
|
||||
/**
|
||||
* Safely serializes an object into bytes.
|
||||
*
|
||||
* @param obj - Object to serialize.
|
||||
*/
|
||||
export function serialize(obj: any): Uint8Array {
|
||||
if (obj instanceof ArrayBuffer || obj instanceof Uint8Array) {
|
||||
return obj instanceof ArrayBuffer ? new Uint8Array(obj) : obj;
|
||||
}
|
||||
@@ -59,67 +64,21 @@ function getObjectAsBytes(obj: any): Uint8Array {
|
||||
return encoder.encode(objJson);
|
||||
}
|
||||
|
||||
function getHeaderBytes(obj: any, metadata?: any): Uint8Array {
|
||||
if (!metadata && (obj instanceof ArrayBuffer || obj instanceof Uint8Array)) {
|
||||
return varint.encode(0);
|
||||
} else {
|
||||
const encoder = new TextEncoder();
|
||||
const headerData = { contentType: 'application/json', ...metadata };
|
||||
const header = JSON.stringify(headerData);
|
||||
const headerVarInt = varint.encode(header.length);
|
||||
return mergeUint8Arrays(headerVarInt, encoder.encode(header));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely serializes an object (and optional metadta) into bytes.
|
||||
*
|
||||
* @param obj - Object to serialize.
|
||||
*/
|
||||
export function serialize(obj: any, metadata?: any): Uint8Array {
|
||||
const headerBytes = getHeaderBytes(obj, metadata);
|
||||
const objAsBytes = getObjectAsBytes(obj);
|
||||
|
||||
return mergeUint8Arrays(headerBytes, objAsBytes);
|
||||
}
|
||||
|
||||
export interface DataWithMetadata {
|
||||
data: any;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely deserializes an object, and possibly any associated metadata, from the
|
||||
* input bytes.
|
||||
* Safely deserializes an object from input bytes.
|
||||
* If the input bytes are valid JSON, the object will be deserialized as JSON.
|
||||
* Otherwise, the input bytes will be returned as-is (Uint8Array).
|
||||
*
|
||||
* @param data - Bytes to deserialize.
|
||||
*/
|
||||
export function deserialize(data: Uint8Array): DataWithMetadata {
|
||||
const { value, bytesProcessed } = varint.decode(data);
|
||||
const headerLength = value;
|
||||
let i = bytesProcessed;
|
||||
if (headerLength === 0) {
|
||||
return { data: data.slice(i) };
|
||||
export function deserialize(data: Uint8Array): any {
|
||||
try {
|
||||
const decoder = new TextDecoder();
|
||||
const obj = JSON.parse(decoder.decode(data));
|
||||
return obj;
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const headerBytes = data.slice(i, i + headerLength);
|
||||
i += headerLength;
|
||||
const header = JSON.parse(decoder.decode(headerBytes));
|
||||
|
||||
const dataBytes = data.slice(i);
|
||||
|
||||
let obj;
|
||||
if (header.contentType === 'application/json') {
|
||||
obj = JSON.parse(decoder.decode(dataBytes));
|
||||
} else {
|
||||
obj = dataBytes;
|
||||
}
|
||||
|
||||
return {
|
||||
data: obj,
|
||||
metadata: header
|
||||
};
|
||||
}
|
||||
|
||||
/** Concatenate the input Uint8Arrays. */
|
||||
|
||||
@@ -3,11 +3,10 @@ import type { KeyInfo } from './bucket/bucket';
|
||||
import type { ApiConfig } from './bucket/bucket_service';
|
||||
import { BucketService } from './bucket/bucket_service';
|
||||
import type { ApiError } from './client/api';
|
||||
import { DataWithMetadata } from './data/serializer';
|
||||
|
||||
export { BucketService as Client, Bucket };
|
||||
|
||||
export type { KeyInfo, BucketService, ApiError, ApiConfig, DataWithMetadata };
|
||||
export type { KeyInfo, BucketService, ApiError, ApiConfig };
|
||||
|
||||
// External copyright notices:
|
||||
/*! pako (C) 1995-2013 Jean-loup Gailly and Mark Adler */
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('serialization/deserialization routines', () => {
|
||||
0,
|
||||
null
|
||||
])(`should be inverses for: %s`, val => {
|
||||
expect(deserialize(serialize(val)).data).toEqual(val);
|
||||
expect(deserialize(serialize(val))).toEqual(val);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"start": "webpack-dev-server --open",
|
||||
"test": "jest",
|
||||
"e2e-tests": "npm link && cd lib/server && cargo build --release && cd ../../e2e-tests && npm link @blyss/sdk && npx ts-node main.ts ../lib/server/target/release/server params",
|
||||
"api-tests": "npm run --silent build && npm link && cd e2e-tests && npm link @blyss/sdk && npx ts-node api.ts",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,4 +61,4 @@
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
python/Cargo.lock
generated
2
python/Cargo.lock
generated
@@ -25,7 +25,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blyss-client-python"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
"spiral-rs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "blyss-client-python"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -18,7 +18,9 @@ from blyss.req_compression import get_session
|
||||
from blyss.bloom import BloomFilter
|
||||
|
||||
CREATE_PATH = "/create"
|
||||
MODIFY_PATH = "/modify"
|
||||
DESTROY_PATH = "/destroy"
|
||||
CLEAR_PATH = "/clear"
|
||||
CHECK_PATH = "/check"
|
||||
LIST_BUCKETS_PATH = "/list-buckets"
|
||||
DELETE_PATH = "/delete"
|
||||
@@ -208,6 +210,16 @@ class API:
|
||||
def _url_for(self, bucket_name: str, path: str) -> str:
|
||||
return self.service_endpoint + "/" + bucket_name + path
|
||||
|
||||
def modify(self, bucket_name: str, data_json: str) -> dict[Any, Any]:
|
||||
"""Modify existing bucket.
|
||||
|
||||
Args:
|
||||
data_json (str): same as create.
|
||||
"""
|
||||
return _post_data_json(
|
||||
self.api_key, self._url_for(bucket_name, MODIFY_PATH), data_json
|
||||
)
|
||||
|
||||
def meta(self, bucket_name: str) -> dict[Any, Any]:
|
||||
"""Get metadata about a bucket.
|
||||
|
||||
@@ -258,6 +270,10 @@ class API:
|
||||
"""Destroy this bucket."""
|
||||
_post_data(self.api_key, self._url_for(bucket_name, DESTROY_PATH), "")
|
||||
|
||||
def clear(self, bucket_name: str):
|
||||
"""Delete all keys in this bucket."""
|
||||
_post_data(self.api_key, self._url_for(bucket_name, CLEAR_PATH), "")
|
||||
|
||||
def write(self, bucket_name: str, data: bytes):
|
||||
"""Write some data to this bucket."""
|
||||
_post_data(self.api_key, self._url_for(bucket_name, WRITE_PATH), data)
|
||||
|
||||
@@ -7,7 +7,7 @@ the compiled Rust code.
|
||||
"""
|
||||
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
from . import blyss, seed # type: ignore
|
||||
|
||||
# NB: There are many "type: ignore"s on purpose. Type information
|
||||
@@ -82,7 +82,7 @@ class BlyssLib:
|
||||
"""
|
||||
return bytes(blyss.decode_response(self.inner_client, response)) # type: ignore
|
||||
|
||||
def extract_result(self, key: str, data: bytes) -> bytes:
|
||||
def extract_result(self, key: str, data: bytes) -> Optional[bytes]:
|
||||
"""Extracts the value for a given key, given the plaintext data from a response.
|
||||
|
||||
Args:
|
||||
@@ -92,7 +92,11 @@ class BlyssLib:
|
||||
Returns:
|
||||
bytes: The plaintext data corresponding to the given key.
|
||||
"""
|
||||
return bytes(blyss.extract_result(self.inner_client, key, data)) # type: ignore
|
||||
r = blyss.extract_result(self.inner_client, key, data)
|
||||
if r is None:
|
||||
return None
|
||||
else:
|
||||
return bytes(r)
|
||||
|
||||
def __init__(self, params: str, secret_seed: str):
|
||||
"""Initializes a new BlyssLib instance.
|
||||
|
||||
@@ -118,7 +118,7 @@ class Bucket:
|
||||
"content-type": "application/octet-stream",
|
||||
}
|
||||
row.append(fmt)
|
||||
row_size += int(72 + len(key) + len(value_str))
|
||||
row_size += int(24 + len(key) + len(value_str) + 48)
|
||||
|
||||
# if the new row doesn't fit into the current chunk, start a new one
|
||||
if current_chunk_size + row_size > _MAX_PAYLOAD:
|
||||
@@ -211,6 +211,14 @@ class Bucket:
|
||||
def list_keys(self) -> dict[str, Any]:
|
||||
"""Gets info on all keys in this bucket."""
|
||||
return self.api.list_keys(self.name)
|
||||
|
||||
def rename(self, new_name: str):
|
||||
"""Renames this bucket."""
|
||||
bucket_create_req = {
|
||||
"name": new_name,
|
||||
}
|
||||
self.api.modify(self.name, json.dumps(bucket_create_req))
|
||||
self.name = new_name
|
||||
|
||||
def write(self, kv_pairs: dict[str, Union[tuple[Any, Optional[Any]], Any]]):
|
||||
"""Writes the supplied key-value pair(s) into the bucket.
|
||||
@@ -251,6 +259,14 @@ class Bucket:
|
||||
"""Destroys the entire bucket. This action is permanent and irreversible."""
|
||||
self.api.destroy(self.name)
|
||||
|
||||
def clear_entire_bucket(self):
|
||||
"""Deletes all keys in this bucket. This action is permanent and irreversible.
|
||||
|
||||
Differs from destroy in that the bucket's metadata
|
||||
(e.g. permissions, PIR scheme parameters, and clients' setup data) are preserved.
|
||||
"""
|
||||
self.api.clear(self.name)
|
||||
|
||||
def private_read(self, keys: Union[str, list[str]]) -> Union[bytes, list[bytes]]:
|
||||
"""Privately reads the supplied key from the bucket,
|
||||
returning the value corresponding to the key.
|
||||
|
||||
@@ -3,7 +3,7 @@ from . import bucket, api, seed
|
||||
import json
|
||||
|
||||
BLYSS_BUCKET_URL = "https://beta.api.blyss.dev"
|
||||
DEFAULT_BUCKET_PARAMETERS = {"maxItemSize": 1000}
|
||||
DEFAULT_BUCKET_PARAMETERS = {"maxItemSize": 1000, "keyStoragePolicy": "bloom"}
|
||||
|
||||
ApiConfig = dict[str, str]
|
||||
|
||||
|
||||
114
python/tests/test_service.py
Normal file
114
python/tests/test_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import hashlib
|
||||
import traceback
|
||||
import blyss
|
||||
|
||||
|
||||
def key_to_gold_value(key: str, length: int = 512) -> bytes:
|
||||
h = hashlib.md5()
|
||||
h.update(key.encode("utf-8"))
|
||||
value = h.digest()
|
||||
while len(value) < length:
|
||||
h.update(b"0")
|
||||
value += h.digest()
|
||||
return value[:length]
|
||||
|
||||
|
||||
def verify_read(key: str, value: bytes):
|
||||
try:
|
||||
assert value == key_to_gold_value(key, len(value))
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def generate_keys(n: int, seed: int = 0) -> list:
|
||||
return [f"{seed}-{i}" for i in range(n)]
|
||||
|
||||
|
||||
def generateBucketName() -> str:
|
||||
tag = int(random.random() * 1e6)
|
||||
return f"api-tester-{tag:#0{6}x}"
|
||||
|
||||
|
||||
async def main(endpoint: str, api_key: str):
|
||||
print("Testing Blyss server at " + endpoint)
|
||||
client = blyss.Client({"endpoint": endpoint, "api_key": api_key})
|
||||
# generate random string for bucket name
|
||||
bucketName = generateBucketName()
|
||||
client.create(bucketName)
|
||||
bucket = client.connect_async(bucketName)
|
||||
print(bucket.info())
|
||||
|
||||
# generate N random keys
|
||||
N = 20000
|
||||
itemSize = 32
|
||||
localKeys = generate_keys(N, 0)
|
||||
# write all N keys
|
||||
await bucket.write({k: key_to_gold_value(k, itemSize) for k in localKeys})
|
||||
print(f"Wrote {N} keys")
|
||||
|
||||
# read a random key
|
||||
testKey = random.choice(localKeys)
|
||||
value = await bucket.private_read([testKey])
|
||||
verify_read(testKey, value[0])
|
||||
print(f"Read key {testKey}")
|
||||
|
||||
# delete testKey from the bucket, and localData.
|
||||
bucket.delete_key(testKey)
|
||||
localKeys.remove(testKey)
|
||||
value = await bucket.private_read([testKey])
|
||||
# TODO: why aren't deletes reflected in the next read?
|
||||
# assert value is None
|
||||
print(f"Deleted key {testKey}")
|
||||
|
||||
# clear all keys
|
||||
bucket.clear_entire_bucket()
|
||||
localKeys = []
|
||||
print("Cleared bucket")
|
||||
|
||||
# write a new set of N keys
|
||||
localKeys = generate_keys(N, 2)
|
||||
await bucket.write({k: key_to_gold_value(k, itemSize) for k in localKeys})
|
||||
print(f"Wrote {N} keys")
|
||||
|
||||
# test if clear took AFTER the new write
|
||||
value = await bucket.private_read([testKey])
|
||||
if value is not None:
|
||||
print(f"ERROR: {testKey} was not deleted or cleared!")
|
||||
|
||||
# rename the bucket
|
||||
newBucketName = bucketName + "-rn"
|
||||
bucket.rename(newBucketName)
|
||||
print("Renamed bucket")
|
||||
print(bucket.info())
|
||||
|
||||
# read a random key
|
||||
testKey = random.choice(localKeys)
|
||||
value = await bucket.private_read([testKey])
|
||||
verify_read(testKey, value[0])
|
||||
print(f"Read key {testKey}")
|
||||
|
||||
# destroy the bucket
|
||||
bucket.destroy_entire_bucket()
|
||||
print("Destroyed bucket")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
api_key = os.environ.get("BLYSS_STAGING_API_KEY", None)
|
||||
endpoint = os.environ.get("BLYSS_STAGING_SERVER", None)
|
||||
if len(sys.argv) > 1:
|
||||
print("Using endpoint from command line")
|
||||
endpoint = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
print("Using api_key from command line")
|
||||
api_key = sys.argv[2]
|
||||
print("DEBUG", api_key, endpoint)
|
||||
assert endpoint is not None
|
||||
assert api_key is not None
|
||||
|
||||
asyncio.run(main(endpoint, api_key))
|
||||
Reference in New Issue
Block a user