Skip to content

Commit d1e7db4

Browse files
SK-3118: Update flowvault README (skyflow import, custom headers, timeouts/retries)
Align the README with the current SDK and Java parity: skyflow import name, table_name, BulkInsertRequestRecord, typed TokenGroupRedactions, and the unary-raises / bulk-inline error model. Add the missing Custom Request Headers and Timeouts & Retries sections, concurrency-sizing guidance, and a scoped-token note; drop the removed CONTRACT_SHAPES.md references. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 661d1e5 commit d1e7db4

1 file changed

Lines changed: 126 additions & 50 deletions

File tree

flowvault/README.md

Lines changed: 126 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ credentials, and configuration with the [skyvault SDK](../skyvault/README.md) (b
55
`common` module) but exposes its own surface: **unary** vault operations plus **bulk** (batched,
66
concurrent) insert and detokenize.
77

8-
> **`skyflow-flowvault-python` is versioned independently of `skyflow`.** It launched at `1.0.0` while
9-
> `skyflow` is at `2.x`. The two are separate artifacts on separate version lines and cannot be
10-
> installed into the same Python environment at once.
8+
> **Install name vs. import name.** You `pip install skyflow-flowvault-python`, then `import skyflow`
9+
> the same import name the `skyflow` (Privacy DB) SDK uses. The two are separate artifacts and **cannot
10+
> be installed into the same Python environment at once** (they'd collide on the `skyflow` package).
1111
1212
## Table of Contents
1313

@@ -16,24 +16,26 @@ concurrent) insert and detokenize.
1616
- [Quickstart](#quickstart)
1717
- [Authenticate](#authenticate)
1818
- [Initialize the client](#initialize-the-client)
19+
- [Timeouts and retries](#timeouts-and-retries)
1920
- [Unary operations](#unary-operations)
2021
- [Insert](#insert) · [Get](#get) · [Update](#update) · [Delete](#delete) · [Detokenize](#detokenize) · [Query](#query)
2122
- [Bulk operations](#bulk-operations)
2223
- [Bulk insert](#bulk-insert) · [Bulk detokenize](#bulk-detokenize)
2324
- [Batching and concurrency](#batching-and-concurrency)
25+
- [Custom request headers](#custom-request-headers)
2426
- [Error handling](#error-handling)
2527
- [Logging](#logging)
2628
- [Samples](#samples)
27-
- [Request / response shapes](#request--response-shapes)
2829

2930
## Overview
3031

3132
- Authenticate with a Skyflow service account, an API key, or a bearer token.
3233
- Perform **unary** operations — insert, get, update, delete, detokenize, query.
3334
- Perform **bulk** operations — insert and detokenize — each with a synchronous and an async
3435
variant, built for high-throughput Flow DB workloads.
35-
- **Per-record reporting, not all-or-nothing.** A call succeeds as a call even when individual
36-
records fail; each response reports the outcome of every record (its own `http_code` and `error`).
36+
- **Unary calls raise on failure; bulk calls report per-record.** A unary API error raises a
37+
`SkyflowError` with the server's details; a bulk call reports every record's outcome inline (its
38+
own `http_code` and `error`) so one bad record or batch never sinks the whole call.
3739

3840
## Install
3941

@@ -46,8 +48,8 @@ Requirements: **Python 3.9+**.
4648
## Quickstart
4749

4850
```python
49-
from skyflow_flowvault import Skyflow, LogLevel, Env
50-
from skyflow_flowvault.vault.data import InsertRequest, InsertRequestRecord
51+
from skyflow import Skyflow, LogLevel, Env
52+
from skyflow.vault.data import InsertRequest, InsertRequestRecord
5153

5254
credentials = {'api_key': '<API_KEY>'} # or 'token' / 'path' / 'credentials_string'
5355

@@ -89,6 +91,9 @@ Set **exactly one** of:
8991
Credentials resolve **most specific first**: per-vault (`vault_config['credentials']`) → client-wide
9092
(`Skyflow.builder().add_skyflow_credentials(...)`) → the `SKYFLOW_CREDENTIALS` environment variable.
9193

94+
For **scoped / context-aware** tokens, add `roles` (list of role ids) and/or `context` to a
95+
`path`/`credentials_string` credentials dict — they are embedded in the generated bearer token.
96+
9297
## Initialize the client
9398

9499
Build the client once and keep it for your application's lifetime; get a controller from it with
@@ -106,20 +111,56 @@ skyflow_client = Skyflow.builder().add_vault_config(vault_config).build()
106111
vault = skyflow_client.vault('<VAULT_ID>')
107112
```
108113

114+
`vault_url` may be set on the vault config to override the URL derived from `cluster_id`/`env`.
115+
116+
## Timeouts and retries
117+
118+
HTTP timeout and retry behavior can be set at **two levels** — per-vault (keys on the vault config
119+
dict) and client-wide (chainable builder methods) — resolved **per field: per-vault → client-wide →
120+
SDK default**.
121+
122+
| Per-vault key | Builder method | Unit | Default | Meaning |
123+
|---|---|---|---|---|
124+
| `timeout` | `.timeout(s)` | seconds | `60` | Overall call ceiling (bounds the whole call incl. retries). |
125+
| `connect_timeout` | `.connect_timeout(s)` | seconds | `10` | Per-attempt connection-establishment timeout. |
126+
| `read_timeout` | `.read_timeout(s)` | seconds | `10` | Per-attempt response-read timeout. |
127+
| `write_timeout` | `.write_timeout(s)` | seconds | `10` | Per-attempt request-write timeout. |
128+
| `max_retries` | `.max_retries(n)` | int ≥ 0 | `0` | Retry attempts after the first failure. `0` = off. |
129+
| `initial_retry_delay_millis` | `.initial_retry_delay_millis(ms)` | int ≥ 0 | `500` | Backoff before the first retry. |
130+
| `max_retry_delay_millis` | `.max_retry_delay_millis(ms)` | int ≥ 0 | `2000` | Ceiling the exponential backoff grows to. |
131+
132+
```python
133+
skyflow_client = (
134+
Skyflow.builder()
135+
.timeout(60).max_retries(3) # client-wide defaults
136+
.add_vault_config({
137+
'vault_id': '<VAULT_ID>', 'cluster_id': '<CLUSTER_ID>', 'env': Env.PROD,
138+
'credentials': {'api_key': '<API_KEY>'},
139+
'read_timeout': 30, 'max_retries': 2, # per-vault overrides
140+
})
141+
.build()
142+
)
143+
```
144+
145+
Retries are **opt-in** (default `0`) so non-idempotent writes are never replayed silently. When
146+
enabled, retryable responses (HTTP `408` / `429` / `5xx`) are retried with exponential backoff and
147+
jitter. A large batch can exceed the default per-attempt timeouts, so raise `read_timeout`/`timeout`
148+
for big bulk calls.
149+
109150
## Unary operations
110151

111-
Every unary response is a single **`records`** list — success and failure inline, one entry per
112-
input, each carrying its own `http_code` and `error`. Exact JSON for each is in
113-
[CONTRACT_SHAPES.md](CONTRACT_SHAPES.md).
152+
A unary API error (the server rejects the call) raises a `SkyflowError` carrying the server's
153+
`http_code`, `message`, `grpc_code`, `http_status`, and `details`. On success the response is a single
154+
**`records`** list — one entry per input, each carrying its own `http_code` and `error`.
114155

115156
### Insert
116157

117158
`table_name`/`upsert` go at **exactly one** level — on the request (applies to all records) or on
118159
every record — never both. `upsert` is an `UpsertOptions`; `tokens` is optional BYOT.
119160

120161
```python
121-
from skyflow_flowvault.vault.data import InsertRequest, InsertRequestRecord, UpsertOptions
122-
from skyflow_flowvault.utils.enums import UpsertType
162+
from skyflow.vault.data import InsertRequest, InsertRequestRecord, UpsertOptions
163+
from skyflow.utils.enums import UpsertType
123164

124165
request = InsertRequest(
125166
table_name='cards',
@@ -128,7 +169,7 @@ request = InsertRequest(
128169
)
129170
response = vault.insert(request)
130171
for r in response.records:
131-
print(r['index'] if 'index' in r else '', r['skyflow_id'], r['tokens'], r['http_code'], r['error'])
172+
print(r['skyflow_id'], r['tokens'], r['http_code'], r['error'])
132173
```
133174
Each record: `{table_name, skyflow_id, tokens, hashed_data, http_code, error}` (no plaintext `data`).
134175

@@ -138,26 +179,26 @@ Two mutually exclusive modes — single-table, or multi-table via `records=[GetR
138179
`column_redactions` entries are `ColumnRedaction` objects.
139180

140181
```python
141-
from skyflow_flowvault.vault.data import GetRequest, GetRecordRequest, ColumnRedaction
182+
from skyflow.vault.data import GetRequest, GetRecordRequest, ColumnRedaction
142183

143184
# single-table
144185
vault.get(GetRequest(
145-
table='persons', ids=['<SKYFLOW_ID>'], columns=['name', 'email'],
186+
table_name='persons', ids=['<SKYFLOW_ID>'], columns=['name', 'email'],
146187
column_redactions=[ColumnRedaction(column_name='email', redaction='MASKED')],
147188
))
148189

149190
# multi-table batch
150191
vault.get(GetRequest(records=[
151-
GetRecordRequest(table='persons', ids=['<SKYFLOW_ID>'], columns=['name']),
152-
GetRecordRequest(table='cards', unique_values=[{'email': 'john@example.com'}]),
192+
GetRecordRequest(table_name='persons', ids=['<SKYFLOW_ID>'], columns=['name']),
193+
GetRecordRequest(table_name='cards', unique_values=[{'email': 'john@example.com'}]),
153194
]))
154195
```
155196
Each record: `{table_name, skyflow_id, tokens, data, hashed_data, http_code, error}`.
156197

157198
### Update
158199

159200
```python
160-
from skyflow_flowvault.vault.data import UpdateRequest
201+
from skyflow.vault.data import UpdateRequest
161202

162203
vault.update(UpdateRequest(
163204
table_name='persons',
@@ -168,28 +209,30 @@ vault.update(UpdateRequest(
168209
### Delete
169210

170211
```python
171-
from skyflow_flowvault.vault.data import DeleteRequest
212+
from skyflow.vault.data import DeleteRequest
172213

173-
vault.delete(DeleteRequest(table='persons', ids=['<SKYFLOW_ID>']))
214+
vault.delete(DeleteRequest(table_name='persons', ids=['<SKYFLOW_ID>']))
174215
```
175216
Each record: `{skyflow_id, http_code, error}`.
176217

177218
### Detokenize
178219

220+
`token_group_redactions` entries are `TokenGroupRedactions` objects.
221+
179222
```python
180-
from skyflow_flowvault.vault.data import DetokenizeRequest
223+
from skyflow.vault.data import DetokenizeRequest, TokenGroupRedactions
181224

182225
vault.detokenize(DetokenizeRequest(
183226
tokens=['<TOKEN>'],
184-
token_group_redactions=[{'token_group_name': 'card_number_cg', 'redaction': 'MASKED'}],
227+
token_group_redactions=[TokenGroupRedactions(token_group_name='card_number_cg', redaction='MASKED')],
185228
))
186229
```
187230
Each record: `{token, token_group_name, value, metadata, http_code, error}`.
188231

189232
### Query
190233

191234
```python
192-
from skyflow_flowvault.vault.data import QueryRequest
235+
from skyflow.vault.data import QueryRequest
193236

194237
response = vault.query(QueryRequest(query="SELECT * FROM persons WHERE skyflow_id = '<SKYFLOW_ID>'"))
195238
print(response.records) # [{'data': {...}}, ...]
@@ -200,16 +243,17 @@ print(response.metadata) # {'columns': [...]}
200243

201244
Bulk operations split the payload into batches sent **concurrently** and return a **`summary`** plus
202245
a **`records`** list — one entry per submitted item, in input order, each tagged with its `index`.
203-
A single bulk call accepts at most **10,000** items.
246+
A single bulk call accepts at most **10,000** items. Unlike unary calls, a bulk call does **not**
247+
raise on a batch API error — every record's outcome is reported inline.
204248

205249
### Bulk insert
206250

207251
```python
208-
from skyflow_flowvault.vault.data import BulkInsertRequest, BulkInsertRecord
252+
from skyflow.vault.data import BulkInsertRequest, BulkInsertRequestRecord
209253

210-
request = BulkInsertRequest(table='cards', records=[
211-
BulkInsertRecord(data={'card_number': '4111111111111111'}),
212-
BulkInsertRecord(data={'card_number': '4222222222222222'}),
254+
request = BulkInsertRequest(table_name='cards', records=[
255+
BulkInsertRequestRecord(data={'card_number': '4111111111111111'}),
256+
BulkInsertRequestRecord(data={'card_number': '4222222222222222'}),
213257
])
214258

215259
response = vault.bulk_insert(request) # synchronous
@@ -221,14 +265,18 @@ for r in response.records:
221265

222266
retry = response.records_to_retry() # original records whose http_code is 500-599 (excl. 529)
223267
```
224-
> `BulkInsertRecord` uses the field name `table` (not `table_name`) and has no `tokens` field.
268+
`BulkInsertRequestRecord(data, table_name=None, tokens=None, upsert=None)` — mirrors Java's
269+
`BulkInsertRequestRecord`; `tokens` is optional BYOT.
225270

226271
### Bulk detokenize
227272

228273
```python
229-
from skyflow_flowvault.vault.data import BulkDetokenizeRequest
274+
from skyflow.vault.data import BulkDetokenizeRequest, TokenGroupRedactions
230275

231-
request = BulkDetokenizeRequest(tokens=['<TOKEN_1>', '<TOKEN_2>'])
276+
request = BulkDetokenizeRequest(
277+
tokens=['<TOKEN_1>', '<TOKEN_2>'],
278+
token_group_redactions=[TokenGroupRedactions(token_group_name='<TOKEN_GROUP_NAME>', redaction='MASKED')],
279+
)
232280

233281
response = vault.bulk_detokenize(request) # synchronous
234282
# response = await vault.bulk_detokenize_async(request) # async variant
@@ -256,28 +304,60 @@ INSERT_BATCH_SIZE=100
256304
INSERT_CONCURRENCY_LIMIT=5
257305
```
258306

307+
**Picking a concurrency value.** A good starting point is the standard formula
308+
`N_concurrency = N_cpu × U_cpu × (1 + W/C)` — where `N_cpu` is the number of cores (`os.cpu_count()`),
309+
`U_cpu` is your target CPU utilization (0–1, ≈1.0 if this is the only workload), and `W/C` is the
310+
ratio of wait time (API latency) to compute time per task. Bulk work is I/O-bound, so `W/C` is large
311+
and concurrency well above core count is usually optimal (up to the max of 10).
312+
259313
Merging is by input order regardless of which batch finishes first, so `index` always matches an
260314
item's position in your submitted payload. A per-batch failure only fails that batch's records.
261315

262-
## Error handling
316+
### Custom request headers
317+
318+
Bulk operations accept an optional `options` object whose **interceptor** runs **once per batch** and
319+
can attach custom headers to that batch's request (mirrors Java's `RequestInterceptor`).
320+
321+
```python
322+
from skyflow.vault.data import BulkInsertOptions, CustomHeaderKey
323+
324+
def add_request_id(context):
325+
# context.operation ('INSERT'/'DETOKENIZE'), context.batch_index, context.total_batches
326+
context.add_header(CustomHeaderKey.REQUEST_ID_HEADER, f'req-{context.batch_index}')
327+
328+
vault.bulk_insert(request, BulkInsertOptions(interceptor=add_request_id))
329+
```
330+
331+
- Options classes: `BulkInsertOptions(interceptor=...)`, `BulkDetokenizeOptions(interceptor=...)`.
332+
- `CustomHeaderKey`: `SKYFLOW_ACCOUNT_ID` (`x-skyflow-account-id`), `SKYFLOW_ACCOUNT_NAME`
333+
(`x-skyflow-account-name`), `REQUEST_ID_HEADER` (`x-request-id`).
334+
- The interceptor runs once per batch, so a value it generates (e.g. a fresh request id) differs
335+
between batches; its headers are merged on top of the SDK's own (metrics + `Authorization`).
263336

264-
Two layers:
337+
## Error handling
265338

266-
- **Request-level** — the call could not be made or wholly failed (invalid request, missing
267-
credentials, auth failure, over the 10,000 ceiling): raised as a `SkyflowError`.
268-
- **Record-level** — the call succeeded but individual records failed: returned in the response.
269-
**Nothing is raised.** Each entry in `records` reports its own `http_code` and `error`.
339+
- **Validation errors** (invalid request, missing credentials, over the 10,000 ceiling) — raised as a
340+
`SkyflowError` before any network call, for every operation.
341+
- **Unary API errors** (the server rejects the call: 4xx/5xx) — raised as a `SkyflowError` with the
342+
server's `http_code`, `message`, `grpc_code`, `http_status`, and `details`.
343+
- **Bulk / per-record failures** — the bulk call itself does not raise; each entry in `records`
344+
reports its own `http_code` and `error`, and `records_to_retry()` / `tokens_to_retry()` return the
345+
inputs worth resending (retryable `5xx`).
270346

271347
```python
272-
from skyflow_flowvault.error import SkyflowError
348+
from skyflow.error import SkyflowError
273349

350+
# unary — an API error raises
274351
try:
275-
response = vault.bulk_insert(request) # reaching here means the CALL succeeded
276-
for r in response.records:
277-
if r['error'] is not None:
278-
print('row', r['index'], 'failed', r['http_code'], r['error'])
352+
response = vault.insert(request)
279353
except SkyflowError as e:
280-
print(e.http_code, e.message, e.details)
354+
print(e.http_code, e.message, e.grpc_code, e.details)
355+
356+
# bulk — inspect per-record outcomes, nothing raised for API errors
357+
response = vault.bulk_insert(bulk_request)
358+
for r in response.records:
359+
if r['error'] is not None:
360+
print('row', r['index'], 'failed', r['http_code'], r['error'])
281361
```
282362

283363
## Logging
@@ -289,9 +369,5 @@ batching warnings above are emitted at `WARN`.
289369
## Samples
290370

291371
Runnable examples live in [samples/](samples/) — one file per operation, with sync/async pairs for
292-
the bulk ops. See [samples/README.md](samples/README.md) to run them.
293-
294-
## Request / response shapes
295-
296-
[CONTRACT_SHAPES.md](CONTRACT_SHAPES.md) documents the exact request and response JSON for every
297-
operation (unary and bulk), for reference and comparison against the Java FlowDB contract.
372+
the bulk ops, plus custom-header, timeout/retry, and service-account examples. See
373+
[samples/README.md](samples/README.md) to run them.

0 commit comments

Comments
 (0)