@@ -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,
66concurrent) 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
5254credentials = {' api_key' : ' <API_KEY>' } # or 'token' / 'path' / 'credentials_string'
5355
@@ -89,6 +91,9 @@ Set **exactly one** of:
8991Credentials 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
9499Build 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()
106111vault = 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
118159every 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
124165request = InsertRequest(
125166 table_name = ' cards' ,
@@ -128,7 +169,7 @@ request = InsertRequest(
128169)
129170response = vault.insert(request)
130171for 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```
133174Each 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
144185vault.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
150191vault.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```
155196Each 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
162203vault.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```
175216Each 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
182225vault.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```
187230Each 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
194237response = vault.query(QueryRequest(query = " SELECT * FROM persons WHERE skyflow_id = '<SKYFLOW_ID>'" ))
195238print (response.records) # [{'data': {...}}, ...]
@@ -200,16 +243,17 @@ print(response.metadata) # {'columns': [...]}
200243
201244Bulk operations split the payload into batches sent ** concurrently** and return a ** ` summary ` ** plus
202245a ** ` 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
215259response = vault.bulk_insert(request) # synchronous
@@ -221,14 +265,18 @@ for r in response.records:
221265
222266retry = 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
233281response = vault.bulk_detokenize(request) # synchronous
234282# response = await vault.bulk_detokenize_async(request) # async variant
@@ -256,28 +304,60 @@ INSERT_BATCH_SIZE=100
256304INSERT_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+
259313Merging is by input order regardless of which batch finishes first, so ` index ` always matches an
260314item'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
274351try :
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)
279353except 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
291371Runnable 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