skyflow-flowvault-python 1.0.0__py3-none-any.whl

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.
Files changed (202) hide show
  1. common/.coveragerc +4 -0
  2. common/__init__.py +3 -0
  3. common/client/__init__.py +0 -0
  4. common/client/base_skyflow.py +345 -0
  5. common/client/utils/__init__.py +1 -0
  6. common/client/utils/_utils.py +127 -0
  7. common/errors/__init__.py +1 -0
  8. common/errors/_skyflow_error.py +17 -0
  9. common/generated/__init__.py +0 -0
  10. common/generated/rest/__init__.py +24 -0
  11. common/generated/rest/authentication/__init__.py +4 -0
  12. common/generated/rest/authentication/client.py +181 -0
  13. common/generated/rest/authentication/raw_client.py +241 -0
  14. common/generated/rest/client.py +153 -0
  15. common/generated/rest/core/__init__.py +52 -0
  16. common/generated/rest/core/api_error.py +23 -0
  17. common/generated/rest/core/client_wrapper.py +86 -0
  18. common/generated/rest/core/datetime_utils.py +28 -0
  19. common/generated/rest/core/file.py +67 -0
  20. common/generated/rest/core/force_multipart.py +16 -0
  21. common/generated/rest/core/http_client.py +543 -0
  22. common/generated/rest/core/http_response.py +55 -0
  23. common/generated/rest/core/jsonable_encoder.py +100 -0
  24. common/generated/rest/core/pydantic_utilities.py +255 -0
  25. common/generated/rest/core/query_encoder.py +58 -0
  26. common/generated/rest/core/remove_none_from_dict.py +11 -0
  27. common/generated/rest/core/request_options.py +35 -0
  28. common/generated/rest/core/serialization.py +276 -0
  29. common/generated/rest/environment.py +8 -0
  30. common/generated/rest/errors/__init__.py +9 -0
  31. common/generated/rest/errors/bad_request_error.py +14 -0
  32. common/generated/rest/errors/not_found_error.py +14 -0
  33. common/generated/rest/errors/unauthorized_error.py +14 -0
  34. common/generated/rest/py.typed +0 -0
  35. common/generated/rest/types/__init__.py +9 -0
  36. common/generated/rest/types/googlerpc_status.py +22 -0
  37. common/generated/rest/types/protobuf_any.py +21 -0
  38. common/generated/rest/types/v_1_get_auth_token_response.py +33 -0
  39. common/generated/rest/version.py +6 -0
  40. common/service_account/__init__.py +1 -0
  41. common/service_account/_utils.py +248 -0
  42. common/service_account/client/__init__.py +0 -0
  43. common/service_account/client/auth_client.py +13 -0
  44. common/utils/__init__.py +4 -0
  45. common/utils/_helpers.py +18 -0
  46. common/utils/_skyflow_messages.py +445 -0
  47. common/utils/_utils.py +50 -0
  48. common/utils/constants.py +291 -0
  49. common/utils/enums/__init__.py +12 -0
  50. common/utils/enums/content_types.py +9 -0
  51. common/utils/enums/detect_entities.py +73 -0
  52. common/utils/enums/detect_output_transcriptions.py +8 -0
  53. common/utils/enums/env.py +13 -0
  54. common/utils/enums/log_level.py +8 -0
  55. common/utils/enums/masking_method.py +5 -0
  56. common/utils/enums/redaction_type.py +7 -0
  57. common/utils/enums/request_method.py +8 -0
  58. common/utils/enums/token_mode.py +6 -0
  59. common/utils/enums/token_type.py +6 -0
  60. common/utils/logger/__init__.py +2 -0
  61. common/utils/logger/_log_helpers.py +47 -0
  62. common/utils/logger/_logger.py +50 -0
  63. common/utils/validations/__init__.py +11 -0
  64. common/utils/validations/_validations.py +267 -0
  65. common/vault/base_vault_client.py +122 -0
  66. common/vault/base_vault_controller.py +58 -0
  67. common/vault/data/__init__.py +2 -0
  68. common/vault/data/_base_insert_request.py +7 -0
  69. common/vault/data/_base_insert_response.py +11 -0
  70. skyflow/__init__.py +2 -0
  71. skyflow/client/__init__.py +1 -0
  72. skyflow/client/_http_config_builder.py +45 -0
  73. skyflow/client/skyflow.py +15 -0
  74. skyflow/error/__init__.py +3 -0
  75. skyflow/generated/__init__.py +0 -0
  76. skyflow/generated/rest/__init__.py +164 -0
  77. skyflow/generated/rest/_default_clients.py +32 -0
  78. skyflow/generated/rest/client.py +385 -0
  79. skyflow/generated/rest/core/__init__.py +127 -0
  80. skyflow/generated/rest/core/api_error.py +23 -0
  81. skyflow/generated/rest/core/client_wrapper.py +148 -0
  82. skyflow/generated/rest/core/datetime_utils.py +70 -0
  83. skyflow/generated/rest/core/file.py +67 -0
  84. skyflow/generated/rest/core/force_multipart.py +18 -0
  85. skyflow/generated/rest/core/http_client.py +940 -0
  86. skyflow/generated/rest/core/http_response.py +63 -0
  87. skyflow/generated/rest/core/http_sse/__init__.py +42 -0
  88. skyflow/generated/rest/core/http_sse/_api.py +455 -0
  89. skyflow/generated/rest/core/http_sse/_decoders.py +74 -0
  90. skyflow/generated/rest/core/http_sse/_exceptions.py +7 -0
  91. skyflow/generated/rest/core/http_sse/_models.py +17 -0
  92. skyflow/generated/rest/core/jsonable_encoder.py +133 -0
  93. skyflow/generated/rest/core/logging.py +107 -0
  94. skyflow/generated/rest/core/parse_error.py +36 -0
  95. skyflow/generated/rest/core/pydantic_utilities.py +486 -0
  96. skyflow/generated/rest/core/query_encoder.py +58 -0
  97. skyflow/generated/rest/core/remove_none_from_dict.py +11 -0
  98. skyflow/generated/rest/core/request_options.py +40 -0
  99. skyflow/generated/rest/core/serialization.py +347 -0
  100. skyflow/generated/rest/environment.py +8 -0
  101. skyflow/generated/rest/errors/__init__.py +53 -0
  102. skyflow/generated/rest/errors/bad_request_error.py +11 -0
  103. skyflow/generated/rest/errors/forbidden_error.py +11 -0
  104. skyflow/generated/rest/errors/internal_server_error.py +11 -0
  105. skyflow/generated/rest/errors/not_found_error.py +11 -0
  106. skyflow/generated/rest/errors/too_many_requests_error.py +11 -0
  107. skyflow/generated/rest/errors/unauthorized_error.py +11 -0
  108. skyflow/generated/rest/py.typed +0 -0
  109. skyflow/generated/rest/query/__init__.py +4 -0
  110. skyflow/generated/rest/query/client.py +139 -0
  111. skyflow/generated/rest/query/raw_client.py +239 -0
  112. skyflow/generated/rest/raw_client.py +86 -0
  113. skyflow/generated/rest/records/__init__.py +34 -0
  114. skyflow/generated/rest/records/client.py +685 -0
  115. skyflow/generated/rest/records/raw_client.py +988 -0
  116. skyflow/generated/rest/records/types/__init__.py +34 -0
  117. skyflow/generated/rest/records/types/update_request_update_type.py +5 -0
  118. skyflow/generated/rest/tokens/__init__.py +4 -0
  119. skyflow/generated/rest/tokens/client.py +246 -0
  120. skyflow/generated/rest/tokens/raw_client.py +507 -0
  121. skyflow/generated/rest/types/__init__.py +116 -0
  122. skyflow/generated/rest/types/column_redactions.py +33 -0
  123. skyflow/generated/rest/types/delete_response.py +23 -0
  124. skyflow/generated/rest/types/delete_response_object.py +42 -0
  125. skyflow/generated/rest/types/detokenize_response.py +23 -0
  126. skyflow/generated/rest/types/detokenize_response_object.py +54 -0
  127. skyflow/generated/rest/types/error_response.py +20 -0
  128. skyflow/generated/rest/types/error_response_error.py +28 -0
  129. skyflow/generated/rest/types/execute_query_record_response.py +22 -0
  130. skyflow/generated/rest/types/execute_query_response.py +26 -0
  131. skyflow/generated/rest/types/execute_query_response_metadata.py +26 -0
  132. skyflow/generated/rest/types/get_request_data.py +60 -0
  133. skyflow/generated/rest/types/get_response.py +23 -0
  134. skyflow/generated/rest/types/get_tokens_from_values_request_object.py +30 -0
  135. skyflow/generated/rest/types/get_tokens_from_values_response.py +23 -0
  136. skyflow/generated/rest/types/google_protobuf_value.py +8 -0
  137. skyflow/generated/rest/types/http_code.py +6 -0
  138. skyflow/generated/rest/types/insert_record_data.py +36 -0
  139. skyflow/generated/rest/types/insert_response.py +23 -0
  140. skyflow/generated/rest/types/record_response_object.py +70 -0
  141. skyflow/generated/rest/types/token_group_redactions.py +33 -0
  142. skyflow/generated/rest/types/tokenize_response_object.py +49 -0
  143. skyflow/generated/rest/types/unique_value.py +22 -0
  144. skyflow/generated/rest/types/update_record_data.py +52 -0
  145. skyflow/generated/rest/types/update_record_data_update_type.py +5 -0
  146. skyflow/generated/rest/types/update_response.py +23 -0
  147. skyflow/generated/rest/types/upsert.py +45 -0
  148. skyflow/generated/rest/types/upsert_update_type.py +5 -0
  149. skyflow/generated/rest/version.py +6 -0
  150. skyflow/service_account/__init__.py +15 -0
  151. skyflow/utils/__init__.py +9 -0
  152. skyflow/utils/_http_config.py +36 -0
  153. skyflow/utils/_response_parsing.py +66 -0
  154. skyflow/utils/_retry.py +97 -0
  155. skyflow/utils/_skyflow_messages.py +114 -0
  156. skyflow/utils/_utils.py +54 -0
  157. skyflow/utils/_version.py +1 -0
  158. skyflow/utils/enums/__init__.py +3 -0
  159. skyflow/utils/enums/_custom_header_key.py +10 -0
  160. skyflow/utils/enums/_env_urls.py +9 -0
  161. skyflow/utils/enums/_upsert_type.py +7 -0
  162. skyflow/utils/validations/__init__.py +9 -0
  163. skyflow/utils/validations/_validations.py +195 -0
  164. skyflow/vault/__init__.py +0 -0
  165. skyflow/vault/client/__init__.py +0 -0
  166. skyflow/vault/client/client.py +102 -0
  167. skyflow/vault/controller/__init__.py +1 -0
  168. skyflow/vault/controller/_vault.py +475 -0
  169. skyflow/vault/data/__init__.py +30 -0
  170. skyflow/vault/data/_column_redactions.py +4 -0
  171. skyflow/vault/data/_delete_options.py +8 -0
  172. skyflow/vault/data/_delete_request.py +5 -0
  173. skyflow/vault/data/_delete_response.py +9 -0
  174. skyflow/vault/data/_delete_response_record.py +9 -0
  175. skyflow/vault/data/_detokenize_options.py +8 -0
  176. skyflow/vault/data/_detokenize_request.py +9 -0
  177. skyflow/vault/data/_detokenize_response.py +9 -0
  178. skyflow/vault/data/_detokenize_response_record.py +16 -0
  179. skyflow/vault/data/_detokenize_response_record_metadata.py +10 -0
  180. skyflow/vault/data/_get_options.py +8 -0
  181. skyflow/vault/data/_get_request.py +17 -0
  182. skyflow/vault/data/_get_request_record.py +13 -0
  183. skyflow/vault/data/_get_response.py +9 -0
  184. skyflow/vault/data/_get_response_record.py +17 -0
  185. skyflow/vault/data/_insert_options.py +8 -0
  186. skyflow/vault/data/_insert_request.py +11 -0
  187. skyflow/vault/data/_insert_request_record.py +9 -0
  188. skyflow/vault/data/_insert_response.py +9 -0
  189. skyflow/vault/data/_insert_response_record.py +17 -0
  190. skyflow/vault/data/_request_context.py +16 -0
  191. skyflow/vault/data/_token.py +8 -0
  192. skyflow/vault/data/_token_group_redactions.py +4 -0
  193. skyflow/vault/data/_update_options.py +8 -0
  194. skyflow/vault/data/_update_request.py +11 -0
  195. skyflow/vault/data/_update_request_record.py +6 -0
  196. skyflow/vault/data/_update_response.py +9 -0
  197. skyflow/vault/data/_update_response_record.py +17 -0
  198. skyflow/vault/data/_upsert_options.py +7 -0
  199. skyflow_flowvault_python-1.0.0.dist-info/METADATA +896 -0
  200. skyflow_flowvault_python-1.0.0.dist-info/RECORD +202 -0
  201. skyflow_flowvault_python-1.0.0.dist-info/WHEEL +5 -0
  202. skyflow_flowvault_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,896 @@
1
+ Metadata-Version: 2.4
2
+ Name: skyflow-flowvault-python
3
+ Version: 1.0.0
4
+ Summary: Skyflow SDK for the Python programming language (v3 / flowservice API)
5
+ Home-page: https://github.com/skyflowapi/skyflow-python/
6
+ Author: Skyflow
7
+ Author-email: service-ops@skyflow.com
8
+ License: LICENSE
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pydantic>=2.0.0
12
+ Requires-Dist: typing-extensions>=4.0.0
13
+ Requires-Dist: PyJWT<3,>=2.12
14
+ Requires-Dist: cryptography>=44.0.2
15
+ Requires-Dist: httpx>=0.21.2
16
+ Provides-Extra: dev
17
+ Requires-Dist: codespell>=2.4.1; extra == "dev"
18
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
19
+ Requires-Dist: pre-commit>=4.3.0; extra == "dev"
20
+ Requires-Dist: griffe==2.2.0; python_version >= "3.10" and extra == "dev"
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: provides-extra
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # Skyflow FlowVault Python SDK
33
+
34
+ The `flowvault` module is a Skyflow Python SDK for vault operations. It shares its client, credentials, and configuration with the [skyvault SDK](../skyvault/README.md) (both depend on the `common` module) but exposes a different, narrower surface: **unary** vault operations — insert, get, update, delete, and detokenize.
35
+
36
+ > Meant for **FlowVault** vaults.
37
+
38
+ > **`skyflow-flowvault-python` is versioned independently of the main `skyflow` SDK.** The two are separate PyPI artifacts with separate version lines, so a lower `flowvault` version number does not mean it is older or behind. Upgrade each artifact on its own; the current release is listed on [PyPI](https://pypi.org/project/skyflow-flowvault-python/).
39
+
40
+ [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-python/actions)
41
+ [![License](https://img.shields.io/github/license/skyflowapi/skyflow-python)](https://github.com/skyflowapi/skyflow-python/blob/main/LICENSE)
42
+
43
+ # Table of Contents
44
+
45
+ - [Table of Contents](#table-of-contents)
46
+ - [Overview](#overview)
47
+ - [Install](#install)
48
+ - [Requirements](#requirements)
49
+ - [Configuration](#configuration)
50
+ - [Quickstart](#quickstart)
51
+ - [Authenticate](#authenticate)
52
+ - [Credential types](#credential-types)
53
+ - [Where credentials can be set](#where-credentials-can-be-set)
54
+ - [Generate a bearer token](#generate-a-bearer-token)
55
+ - [Generate bearer tokens with context](#generate-bearer-tokens-with-context)
56
+ - [Generate scoped bearer tokens](#generate-scoped-bearer-tokens)
57
+ - [Generate signed data tokens](#generate-signed-data-tokens)
58
+ - [Initialize the client](#initialize-the-client)
59
+ - [VaultConfig reference](#vaultconfig-reference)
60
+ - [Skyflow.builder() reference](#skyflowbuilder-reference)
61
+ - [Timeouts and retries](#timeouts-and-retries)
62
+ - [Logging](#logging)
63
+ - [Thread safety and resource lifecycle](#thread-safety-and-resource-lifecycle)
64
+ - [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults)
65
+ - [VaultController — Unary operations](#vaultcontroller--unary-operations)
66
+ - [Vault type support](#vault-type-support)
67
+ - [Insert](#insert)
68
+ - [Detokenize](#detokenize)
69
+ - [Get](#get)
70
+ - [Update](#update)
71
+ - [Delete](#delete)
72
+ - [Custom Request Headers](#custom-request-headers)
73
+ - [Error Handling](#error-handling)
74
+ - [Two layers of errors](#two-layers-of-errors)
75
+ - [Per-record success and failure](#per-record-success-and-failure)
76
+ - [Catching SkyflowError](#catching-skyflowerror)
77
+ - [SkyflowError properties](#skyflowerror-properties)
78
+ - [Retrying the failed records](#retrying-the-failed-records)
79
+ - [Samples](#samples)
80
+
81
+ # Overview
82
+
83
+ - Authenticate using a Skyflow service account, an API key, or a bearer token — see [Authenticate](#authenticate).
84
+ - Perform unary Vault API operations — insert, get, update, delete, and detokenize — a single API call each. See [VaultController — Unary operations](#vaultcontroller--unary-operations).
85
+ - **Per-record reporting, not all-or-nothing.** A call succeeds as a call even when individual records fail; every response reports the outcome of each individual record or token. See [Error Handling](#error-handling).
86
+
87
+ # Install
88
+
89
+ ## Requirements
90
+
91
+ - Python 3.9 and above
92
+
93
+ ## Configuration
94
+
95
+ ```bash
96
+ pip install skyflow-flowvault-python
97
+ ```
98
+
99
+ The package installs under the import name `skyflow`:
100
+
101
+ ```python
102
+ from skyflow import Skyflow, LogLevel, Env
103
+ ```
104
+
105
+ > **Do not install this alongside the main `skyflow` SDK.** This package and the main [skyflow SDK](../skyvault/README.md) (published to PyPI as `skyflow`) both ship the same top-level `skyflow` import package, so installing both in one environment makes them shadow each other — whichever was installed last wins, and imports resolve to the wrong SDK. Pick the one you need per environment, and if you need both, keep them in separate virtual environments.
106
+
107
+ Check [PyPI](https://pypi.org/project/skyflow-flowvault-python/) for the current version and [GitHub releases](https://github.com/skyflowapi/skyflow-python/releases) for what changed in each one.
108
+
109
+ # Quickstart
110
+
111
+ ```python
112
+ from skyflow import Skyflow, LogLevel, Env
113
+ from skyflow.vault.data import InsertRequest, InsertRequestRecord
114
+
115
+ credentials = {'api_key': '<API_KEY>'} # or 'token' / 'path' / 'credentials_string'
116
+
117
+ vault_config = {
118
+ 'vault_id': '<VAULT_ID>',
119
+ 'cluster_id': '<CLUSTER_ID>', # part of the vault URL: https://{cluster_id}.vault.skyflowapis.com
120
+ 'env': Env.PROD, # DEV, STAGE, SANDBOX, or PROD (default)
121
+ 'credentials': credentials,
122
+ }
123
+
124
+ skyflow_client = (
125
+ Skyflow.builder()
126
+ .add_vault_config(vault_config)
127
+ .build()
128
+ )
129
+
130
+ # Returns the controller for the given vault
131
+ vault = skyflow_client.vault('<VAULT_ID>')
132
+
133
+ response = vault.insert(InsertRequest(
134
+ table_name='cards',
135
+ records=[InsertRequestRecord(data={'card_number': '4111111111111111'})],
136
+ ))
137
+ print(response.records)
138
+ ```
139
+
140
+ `vault('<VAULT_ID>')` returns the controller for a specific registered vault. `vault()` with no argument returns the controller for the first vault added to the builder. To talk to more than one vault from a single client, register each with `add_vault_config(...)` and fetch each controller by ID.
141
+
142
+ # Authenticate
143
+
144
+ Requests are authorized with Skyflow credentials that you attach to the vault config's `credentials` dict. Credential handling comes from the shared `common` module, so it works the same way `skyvault` does.
145
+
146
+ ## Credential types
147
+
148
+ Set **exactly one** of the following keys on the `credentials` dict. If you set more than one, the resolution is undefined — pick one.
149
+
150
+ | Key | What it is |
151
+ |---|---|
152
+ | `api_key` | A long-lived key that authenticates and authorizes requests to the API. Simplest option. |
153
+ | `token` | A short-lived bearer token, typically generated from service account credentials. See [Generate a bearer token](#generate-a-bearer-token). |
154
+ | `path` | Filesystem path to a service account `credentials.json`. The SDK generates and refreshes bearer tokens from it. |
155
+ | `credentials_string` | The contents of a service account `credentials.json` as a JSON string — use this when the credentials come from a secret store rather than a file. |
156
+
157
+ Two optional modifiers apply when the SDK is generating tokens for you (that is, with `path` or `credentials_string`):
158
+
159
+ | Key | Description |
160
+ |---|---|
161
+ | `roles` | Restrict the generated token to specific role IDs (a scoped token). |
162
+ | `context` | Attach context to the generated token for context-aware authorization. |
163
+
164
+ ```python
165
+ # API key
166
+ credentials = {'api_key': '<API_KEY>'}
167
+
168
+ # Bearer token you generated yourself
169
+ credentials = {'token': '<BEARER_TOKEN>'}
170
+
171
+ # Service account credentials file — the SDK handles token generation and refresh
172
+ credentials = {'path': '<PATH_TO_CREDENTIALS_JSON>'}
173
+
174
+ # Service account credentials as a JSON string
175
+ credentials = {'credentials_string': '<CREDENTIALS_JSON_STRING>'}
176
+ ```
177
+
178
+ ## Where credentials can be set
179
+
180
+ Credentials resolve **most specific first**:
181
+
182
+ 1. **Per-vault** — `vault_config['credentials']`. Wins for that vault.
183
+ 2. **Client-wide** — `Skyflow.builder().add_skyflow_credentials(credentials)`. Used by any vault that has none of its own.
184
+ 3. **Environment** — if neither is provided, the SDK reads the `SKYFLOW_CREDENTIALS` environment variable.
185
+
186
+ If none of the three yields credentials, the call fails with a `SkyflowError`.
187
+
188
+ ## Generate a bearer token
189
+
190
+ If you would rather manage tokens yourself, the service-account utilities ship inside `skyflow-flowvault-python` under `skyflow.service_account`. They are plain functions:
191
+
192
+ - `generate_bearer_token(path, options=None)` — mint a token from a service account credentials **file path**.
193
+ - `generate_bearer_token_from_creds(credentials_string, options=None)` — the same, from the credentials JSON as a **string**.
194
+ - `is_expired(token)` — `True` when the token is empty or past expiry; use it to reuse a cached token until it expires.
195
+ - `generate_signed_data_tokens(path, options)` / `generate_signed_data_tokens_from_creds(credentials_string, options)` — see [Generate signed data tokens](#generate-signed-data-tokens).
196
+
197
+ Each `generate_bearer_token*` call returns a `(token, ...)` tuple — unpack the first element. Tokens are valid for 60 minutes and carry the service account's permissions.
198
+
199
+ [Example](https://github.com/skyflowapi/skyflow-python/blob/main/flowvault/samples/service_account/bearer_token_generation_example.py):
200
+
201
+ ```python
202
+ from skyflow.service_account import generate_bearer_token, generate_bearer_token_from_creds, is_expired
203
+
204
+ bearer_token = ''
205
+
206
+ def get_token(file_path):
207
+ global bearer_token
208
+ if not is_expired(bearer_token):
209
+ return bearer_token # reuse until it expires
210
+ token, _ = generate_bearer_token(file_path)
211
+ bearer_token = token
212
+ return bearer_token
213
+
214
+ # ...or from a credentials JSON string
215
+ token, _ = generate_bearer_token_from_creds(credentials_string)
216
+ ```
217
+
218
+ To use the token with the SDK, put it on the `credentials` dict:
219
+
220
+ ```python
221
+ credentials = {'token': token}
222
+ ```
223
+
224
+ ## Generate bearer tokens with context
225
+
226
+ **Context-aware authorization** embeds context values into a bearer token during generation so your policies can reference them. This enables more flexible access controls, such as tracking end-user identity when calling through a service account, and is required for detokenizing signed data tokens.
227
+
228
+ Pass an `options` dict with a `ctx` key — either a **string** or a **dict**:
229
+
230
+ ```python
231
+ # String context — a single value your policy references as request.context
232
+ token, _ = generate_bearer_token(file_path, {'ctx': 'user_12345'})
233
+
234
+ # Dict context — each key maps to a Skyflow CEL policy variable under request.context.*
235
+ token, _ = generate_bearer_token(file_path, {'ctx': {
236
+ 'role': 'admin',
237
+ 'department': 'finance',
238
+ 'user_id': 'user_12345',
239
+ }})
240
+ ```
241
+
242
+ With the dict above, your Skyflow policies can reference `request.context.role`, `request.context.department`, and `request.context.user_id` to make conditional access decisions.
243
+
244
+ [Full example](https://github.com/skyflowapi/skyflow-python/blob/main/flowvault/samples/service_account/bearer_token_generation_with_context_example.py)
245
+
246
+ ## Generate scoped bearer tokens
247
+
248
+ A service account with multiple roles can generate bearer tokens limited to specific roles by passing their role IDs in `options['role_ids']`. This is useful for services with several responsibilities, such as separating billing access from analytics access. The generated tokens are valid for 60 minutes and can only execute operations permitted by the designated roles.
249
+
250
+ [Example](https://github.com/skyflowapi/skyflow-python/blob/main/flowvault/samples/service_account/scoped_token_generation_example.py):
251
+
252
+ ```python
253
+ from skyflow.service_account import generate_bearer_token
254
+
255
+ options = {'role_ids': ['<YOUR_ROLE_ID1>', '<YOUR_ROLE_ID2>']}
256
+ token, _ = generate_bearer_token(file_path, options)
257
+ # ...or from a credentials string: generate_bearer_token_from_creds(credentials_string, options)
258
+ ```
259
+
260
+ To generate bearer tokens concurrently from several threads, see [bearer_token_generation_using_threads_example.py](https://github.com/skyflowapi/skyflow-python/blob/main/flowvault/samples/service_account/bearer_token_generation_using_threads_example.py).
261
+
262
+ ## Generate signed data tokens
263
+
264
+ Skyflow generates data tokens when sensitive data is inserted into the vault. Those data tokens can be digitally signed with the private key of the service account credentials, which adds a further layer of protection. A signed token can only be detokenized by passing it together with a bearer token generated from service account credentials that hold the matching context and permissions.
265
+
266
+ `options` accepts `ctx` (string or dict, same format as bearer tokens), `data_tokens` (the tokens to sign), and `time_to_live` (seconds):
267
+
268
+ ```python
269
+ from skyflow.service_account import generate_signed_data_tokens, generate_signed_data_tokens_from_creds
270
+
271
+ options = {
272
+ 'ctx': 'user_12345',
273
+ 'data_tokens': ['<DATA_TOKEN1>', '<DATA_TOKEN2>'],
274
+ 'time_to_live': 30, # seconds
275
+ }
276
+ results = generate_signed_data_tokens(file_path, options)
277
+ # ...or from a credentials string: generate_signed_data_tokens_from_creds(credentials_string, options)
278
+
279
+ for data_token, signed_data_token in results:
280
+ print(data_token, '->', signed_data_token)
281
+ ```
282
+
283
+ `generate_signed_data_tokens*` returns a list of `(data_token, signed_data_token)` tuples.
284
+
285
+ [Full example](https://github.com/skyflowapi/skyflow-python/blob/main/flowvault/samples/service_account/signed_token_generation_example.py)
286
+
287
+ # Initialize the client
288
+
289
+ `Skyflow` is the client. Build it once, keep it for the lifetime of your application, and get a `VaultController` from it with `vault(...)`.
290
+
291
+ ```python
292
+ from skyflow import Skyflow, LogLevel, Env
293
+
294
+ # Step 1: Credentials — exactly one credential type
295
+ credentials = {'path': '<PATH_TO_CREDENTIALS_JSON>'}
296
+
297
+ # Step 2: Vault configuration
298
+ vault_config = {
299
+ 'vault_id': '<VAULT_ID>',
300
+ 'cluster_id': '<CLUSTER_ID>',
301
+ 'env': Env.PROD, # DEV, STAGE, SANDBOX, or PROD (default)
302
+ 'credentials': credentials,
303
+ # Optional: vault-level HTTP overrides
304
+ 'timeout': 120, # overall call timeout, in seconds
305
+ 'max_retries': 2, # retries after the first failure
306
+ }
307
+
308
+ # Step 3: Build the client
309
+ skyflow_client = (
310
+ Skyflow.builder()
311
+ .set_log_level(LogLevel.INFO) # default is ERROR
312
+ .add_vault_config(vault_config)
313
+ .build()
314
+ )
315
+
316
+ # Step 4: Get the controller and issue calls
317
+ vault = skyflow_client.vault('<VAULT_ID>')
318
+ ```
319
+
320
+ ## VaultConfig reference
321
+
322
+ The vault config is a plain dict:
323
+
324
+ | Key | Required | Description |
325
+ |---|---|---|
326
+ | `vault_id` | required | The vault's ID. |
327
+ | `cluster_id` | required | The cluster portion of the vault URL — `https://{cluster_id}.vault.skyflowapis.com`. |
328
+ | `env` | optional | `Env.DEV`, `Env.STAGE`, `Env.SANDBOX`, or `Env.PROD`. Defaults to `PROD`. |
329
+ | `credentials` | optional | Credentials for this vault. Falls back to client-wide credentials, then `SKYFLOW_CREDENTIALS`. |
330
+ | `vault_url` | optional | Full vault URL, when it cannot be derived from `cluster_id` and `env`. |
331
+ | `timeout` | optional | Overall call timeout in seconds, including retries. |
332
+ | `connect_timeout` | optional | Per-attempt connection timeout, in seconds. |
333
+ | `read_timeout` | optional | Per-attempt response-read timeout, in seconds. |
334
+ | `write_timeout` | optional | Per-attempt request-write timeout, in seconds. |
335
+ | `max_retries` | optional | Retry attempts after the first failure. |
336
+ | `initial_retry_delay_millis` | optional | Backoff before the first retry, in milliseconds. |
337
+ | `max_retry_delay_millis` | optional | Ceiling the exponential backoff grows to, in milliseconds. |
338
+
339
+ ## Skyflow.builder() reference
340
+
341
+ | Method | Description |
342
+ |---|---|
343
+ | `add_vault_config(config)` | Register a vault. The first one registered is what `vault()` (no argument) returns. |
344
+ | `update_vault_config(config)` | Update a registered vault in place. |
345
+ | `remove_vault_config(vault_id)` | Unregister a vault. |
346
+ | `add_skyflow_credentials(credentials)` | Client-wide credentials for vaults that don't set their own. |
347
+ | `set_log_level(log_level)` | `LogLevel.DEBUG`, `INFO`, `WARN`, `ERROR` (default), or `OFF`. |
348
+ | `timeout(s)` / `connect_timeout(s)` / `read_timeout(s)` / `write_timeout(s)` | Client-wide HTTP timeouts, in seconds. |
349
+ | `max_retries(n)` / `initial_retry_delay_millis(ms)` / `max_retry_delay_millis(ms)` | Client-wide retry policy. |
350
+ | `build()` | Produce the `Skyflow` client. |
351
+
352
+ Once built, `skyflow_client.vault('<VAULT_ID>')` returns the controller for a specific registered vault, which is how one client talks to more than one vault.
353
+
354
+ ## Timeouts and retries
355
+
356
+ Each HTTP setting resolves **most specific first**: the value on the vault config, else the client-wide value on `Skyflow.builder()`, else the SDK default.
357
+
358
+ | Setting | SDK default |
359
+ |---|---|
360
+ | `timeout` (overall call, incl. retries) | 60 s |
361
+ | `connect_timeout` / `read_timeout` / `write_timeout` (per attempt) | 10 s |
362
+ | `max_retries` | `0` — retries are **opt-in**, so non-idempotent writes are never replayed silently |
363
+ | `initial_retry_delay_millis` | 500 ms |
364
+ | `max_retry_delay_millis` | 2000 ms |
365
+
366
+ ```python
367
+ skyflow_client = (
368
+ Skyflow.builder()
369
+ .timeout(60).max_retries(3) # client-wide policy
370
+ .initial_retry_delay_millis(500).max_retry_delay_millis(4000)
371
+ .add_vault_config({
372
+ 'vault_id': '<VAULT_ID>', 'cluster_id': '<CLUSTER_ID>', 'env': Env.PROD,
373
+ 'credentials': {'api_key': '<API_KEY>'},
374
+ 'timeout': 300, 'read_timeout': 30, # per-vault overrides
375
+ })
376
+ .build()
377
+ )
378
+ ```
379
+
380
+ When retries are enabled, retryable responses (HTTP `408` / `429` / `5xx`) are retried with exponential backoff and jitter, bounded by the `initial`/`max` delays and the overall `timeout`.
381
+
382
+ ## Logging
383
+
384
+ The SDK logs at `LogLevel.ERROR` by default. Levels rank `DEBUG` < `INFO` < `WARN` < `ERROR` < `OFF`; setting a level prints that level and everything above it. Change it with `Skyflow.builder().set_log_level(LogLevel.DEBUG)`.
385
+
386
+ ## Thread safety and resource lifecycle
387
+
388
+ - **Thread safety** — `Skyflow` and every `VaultController` it hands out are meant to be built once and reused for the application's lifetime; the underlying HTTP client is reused across calls rather than recreated per request.
389
+
390
+ ## Schema vs. schemaless vaults
391
+
392
+ Which operations make sense depends on whether the vault is **structured** (has a schema — tables and columns) or **schemaless** (stores standalone tokens with no table structure): the record operations (`insert`, `get`, `update`, `delete`) address a table's columns and so need a structured vault, while `detokenize` needs only the token itself and works against either kind. See [Vault type support](#vault-type-support) for the per-operation breakdown.
393
+
394
+ # VaultController — Unary operations
395
+
396
+ `VaultController` is returned by `skyflow_client.vault(...)` and exposes these vault operations. Each sends exactly one API call and hands the result straight back:
397
+
398
+ | Method | Parameters | Returns | Description |
399
+ |--------|-----------|---------|-------------|
400
+ | `insert(request)` | `InsertRequest`, optional `InsertOptions` | `InsertResponse` | Insert records, optionally across multiple tables, in one call |
401
+ | `detokenize(request)` | `DetokenizeRequest`, optional `DetokenizeOptions` | `DetokenizeResponse` | Detokenize tokens, optionally with a redaction override per token group |
402
+ | `get(request)` | `GetRequest`, optional `GetOptions` | `GetResponse` | Read records by skyflow ID or unique value, optionally with a redaction override per column |
403
+ | `update(request)` | `UpdateRequest`, optional `UpdateOptions` | `UpdateResponse` | Update records by skyflow ID |
404
+ | `delete(request)` | `DeleteRequest`, optional `DeleteOptions` | `DeleteResponse` | Delete records by skyflow ID or unique value |
405
+
406
+ Each method also accepts an optional options object (`InsertOptions`, `DetokenizeOptions`, `GetOptions`, `UpdateOptions`, `DeleteOptions`) — see [Custom Request Headers](#custom-request-headers).
407
+
408
+ ## Vault type support
409
+
410
+ The same distinction as [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults) applies. Four of the five unary operations address records inside a table, so they only make sense against a structured vault:
411
+
412
+ | Operation | Supported on |
413
+ |---|---|
414
+ | `insert` | Structured (schema) vaults — inserts into a table's columns. |
415
+ | `get` | Structured vaults — reads a table's records by skyflow ID or unique value. |
416
+ | `update` | Structured vaults — updates a table's records by skyflow ID. |
417
+ | `delete` | Structured vaults — deletes a table's records. |
418
+ | `detokenize` | Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. |
419
+
420
+ # Insert
421
+
422
+ Insert records in a single API call. Each record is an `InsertRequestRecord` with its own `data` and, optionally, its own `table_name`, `tokens`, and `upsert`.
423
+
424
+ > **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support).
425
+
426
+ **Note:**
427
+
428
+ - `table_name`/`upsert` follow a one-level rule: on the request (applies to all records) or on every record — never both.
429
+ - `tokens` is optional bring-your-own-token; when supplied, the map must not be empty and no key or value may be blank.
430
+
431
+ ```python
432
+ from skyflow.vault.data import InsertRequest, InsertRequestRecord, UpsertOptions
433
+ from skyflow.utils.enums import UpsertType
434
+
435
+ vault = skyflow_client.vault('<VAULT_ID>') # skyflow_client from Quickstart
436
+
437
+ # Insert several records into one table — set table_name once on the request
438
+ request = InsertRequest(
439
+ table_name='cards',
440
+ records=[
441
+ InsertRequestRecord(data={'card_number': '4111111111111111', 'cardholder_name': 'john doe'}),
442
+ InsertRequestRecord(data={'card_number': '4222222222222222', 'cardholder_name': 'jane doe'}),
443
+ ],
444
+ )
445
+ response = vault.insert(request)
446
+ ```
447
+
448
+ To put the table name on each record instead — the same one-level rule — drop the request-level `table_name` and set it on **every** record. This is also how you insert across different tables in one call, and where per-record `upsert` goes:
449
+
450
+ ```python
451
+ request = InsertRequest(records=[
452
+ # table_name lives on each record here
453
+ InsertRequestRecord(table_name='table1', data={'card_number': '4111111111111111', 'cardholder_name': 'john doe'}),
454
+ InsertRequestRecord(
455
+ table_name='table2',
456
+ data={'email': 'jane.doe@example.com'},
457
+ upsert=UpsertOptions(unique_columns=['email'], update_type=UpsertType.UPDATE),
458
+ ),
459
+ ])
460
+ response = vault.insert(request)
461
+ ```
462
+
463
+ There is no async variant: `insert` returns its `InsertResponse` directly.
464
+
465
+ Sample response:
466
+
467
+ ```json
468
+ {
469
+ "records": [
470
+ {
471
+ "table_name": "cards",
472
+ "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
473
+ "tokens": {
474
+ "card_number": [ { "token": "5484-7829-1702-9110", "token_group_name": "card_number_cg", "path": null } ]
475
+ },
476
+ "data": { "card_number": "4111111111111111", "cardholder_name": "john doe" },
477
+ "hashed_data": { "card_number": [ { "data": "b6e6d...c3f9", "hash_name": "hash1" } ] },
478
+ "http_code": 200,
479
+ "error": null,
480
+ "request_id": null
481
+ },
482
+ {
483
+ "table_name": "cards",
484
+ "skyflow_id": "1c7e4f02-9a3b-4d18-8f21-6b0c9d5e3a44",
485
+ "tokens": {
486
+ "card_number": [ { "token": "6011-3821-4490-7752", "token_group_name": "card_number_cg", "path": null } ]
487
+ },
488
+ "data": { "card_number": "4222222222222222", "cardholder_name": "jane doe" },
489
+ "hashed_data": { "card_number": [ { "data": "1a2b3...9f0e", "hash_name": "hash1" } ] },
490
+ "http_code": 200,
491
+ "error": null,
492
+ "request_id": null
493
+ }
494
+ ]
495
+ }
496
+ ```
497
+
498
+ The records come back in the order you submitted them. `request_id` is `None` on success and the failing call's `x-request-id` on error. `.tokens` maps each column to a **list** of `Token` objects — one entry per token group configured on that column — each with `.token`, `.token_group_name`, and `.path` (the location within a structured column's value the token came from, e.g. `"phone_numbers[0].type"`; `None` for a flat column). Insert records also carry `.data`, the stored column values the vault returns.
499
+
500
+ ```python
501
+ for record in response.records:
502
+ if record.error is None:
503
+ print(record.skyflow_id, record.tokens)
504
+ else:
505
+ print('insert failed', record.http_code, record.error)
506
+ ```
507
+
508
+ Accessors on each `InsertResponseRecord`: `.table_name`, `.skyflow_id`, `.tokens`, `.data`, `.hashed_data`, `.http_code`, `.error`, `.request_id`.
509
+
510
+ # Detokenize
511
+
512
+ Detokenize tokens in a single API call, optionally overriding the redaction applied per token group via `token_group_redactions`.
513
+
514
+ > **Vault type supported:** both. See [Vault type support](#vault-type-support).
515
+
516
+ **Note:**
517
+
518
+ - `tokens` is required and must not be empty, and no entry may be blank.
519
+ - `token_group_redactions` is optional; when supplied, each entry needs a non-blank `token_group_name` and `redaction`. Entries are `TokenGroupRedactions` objects.
520
+
521
+ ```python
522
+ from skyflow.vault.data import DetokenizeRequest, TokenGroupRedactions
523
+
524
+ vault = skyflow_client.vault('<VAULT_ID>') # skyflow_client from Quickstart
525
+
526
+ request = DetokenizeRequest(
527
+ tokens=['5479-4229-4622-1393', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'],
528
+ token_group_redactions=[TokenGroupRedactions(token_group_name='card_number_cg', redaction='MASKED')],
529
+ )
530
+ response = vault.detokenize(request)
531
+ ```
532
+
533
+ There is no async variant: `detokenize` returns its `DetokenizeResponse` directly.
534
+
535
+ Sample response:
536
+
537
+ ```json
538
+ {
539
+ "records": [
540
+ {
541
+ "token": "5479-4229-4622-1393",
542
+ "value": "4111111111111111",
543
+ "token_group_name": "card_number_cg",
544
+ "metadata": { "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "table_name": "table1" },
545
+ "http_code": 200,
546
+ "error": null,
547
+ "request_id": null
548
+ },
549
+ {
550
+ "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
551
+ "value": null,
552
+ "token_group_name": null,
553
+ "metadata": null,
554
+ "http_code": 404,
555
+ "error": "Token Not Found",
556
+ "request_id": "a1b2c3d4-..."
557
+ }
558
+ ]
559
+ }
560
+ ```
561
+
562
+ `record.metadata` is a typed `DetokenizeResponseRecordMetadata` with `.skyflow_id` / `.table_name` (`None` on records that errored).
563
+
564
+ ```python
565
+ for record in response.records:
566
+ if record.error is None:
567
+ print(record.token, '->', record.value, '(', record.token_group_name, ')')
568
+ else:
569
+ print(record.token, 'failed', record.http_code, record.error)
570
+ ```
571
+
572
+ Accessors on each `DetokenizeResponseRecord`: `.token`, `.value`, `.token_group_name`, `.metadata`, `.http_code`, `.error`, `.request_id`.
573
+
574
+ # Get
575
+
576
+ Read records back from a table, by skyflow ID or by unique value, optionally overriding the redaction applied per column via `column_redactions`.
577
+
578
+ > **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support).
579
+
580
+ **Note:**
581
+
582
+ - A `GetRequest` works in one of two modes, and they are mutually exclusive: **single-table** (`table_name`, `skyflow_ids`/`unique_values`, `columns`, `column_redactions`, `limit`, `offset`) or **multi-table** (`records`, a list of `GetRequestRecord`). Setting fields from both modes fails validation.
583
+ - `table_name` is required, and exactly one of `skyflow_ids` or `unique_values` must be supplied — both, or neither, fails validation. This holds per record in multi-table mode.
584
+ - `unique_values` is a list of dicts: one dict per record, each holding the unique column-name/value pairs that identify it.
585
+ - `columns` selects the columns to return; omit it for all of them.
586
+ - `limit` and `offset` apply to the call as a whole and are **only used in single-table mode**.
587
+ - `column_redactions` entries are `ColumnRedactions` objects.
588
+
589
+ ```python
590
+ from skyflow.vault.data import GetRequest, GetRequestRecord, ColumnRedactions
591
+
592
+ vault = skyflow_client.vault('<VAULT_ID>') # skyflow_client from Quickstart
593
+
594
+ # single-table, by skyflow ID
595
+ vault.get(GetRequest(
596
+ table_name='table1',
597
+ skyflow_ids=['9fac9201-7b8a-4446-93f8-5244e1213bd1'],
598
+ columns=['card_number', 'cardholder_name'],
599
+ column_redactions=[ColumnRedactions(column_name='card_number', redaction='MASKED')],
600
+ limit=10, offset=0,
601
+ ))
602
+
603
+ # single-table, by unique value
604
+ vault.get(GetRequest(table_name='table2', unique_values=[{'email': 'jane.doe@example.com'}]))
605
+
606
+ # multi-table batch — each GetRequestRecord carries its own table and lookup fields
607
+ vault.get(GetRequest(records=[
608
+ GetRequestRecord(table_name='table1', skyflow_ids=['9fac9201-...'], columns=['card_number']),
609
+ GetRequestRecord(table_name='table2', unique_values=[{'email': 'jane.doe@example.com'}]),
610
+ ]))
611
+ ```
612
+
613
+ There is no async variant: `get` returns its `GetResponse` directly.
614
+
615
+ Sample response:
616
+
617
+ ```json
618
+ {
619
+ "records": [
620
+ {
621
+ "table_name": "table1",
622
+ "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
623
+ "tokens": {
624
+ "card_number": [ { "token": "5484-7829-1702-9110", "token_group_name": "card_number_cg", "path": null } ]
625
+ },
626
+ "data": { "card_number": "4111-XXXX-XXXX-1111", "cardholder_name": "John Doe" },
627
+ "hashed_data": null,
628
+ "http_code": 200,
629
+ "error": null,
630
+ "request_id": null
631
+ }
632
+ ]
633
+ }
634
+ ```
635
+
636
+ `GetResponseRecord` carries the same fields as an insert record, plus plaintext `.data`: `.table_name`, `.skyflow_id`, `.tokens`, `.data`, `.hashed_data`, `.http_code`, `.error`, `.request_id`.
637
+
638
+ ```python
639
+ for record in response.records:
640
+ if record.error is None:
641
+ print(record.skyflow_id, '->', record.data)
642
+ else:
643
+ print(record.skyflow_id, 'failed', record.http_code, record.error)
644
+ ```
645
+
646
+ # Update
647
+
648
+ Update records in a table by skyflow ID, in a single API call.
649
+
650
+ > **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support).
651
+
652
+ **Note:**
653
+
654
+ - `table_name` is required on the request. A record may override it with its own `table_name`, which applies to that record only.
655
+ - Every `UpdateRequestRecord` needs a non-blank `skyflow_id`.
656
+ - `data` holds the columns to change; no key or value may be blank. `tokens` is optional bring-your-own-token.
657
+ - `update_type` accepts `UpsertType.UPDATE` (merge the supplied columns) or `UpsertType.REPLACE` (overwrite the whole record). Omitting it sends no `update_type`, which the vault treats the same as an update. It is a request-level setting.
658
+
659
+ ```python
660
+ from skyflow.vault.data import UpdateRequest, UpdateRequestRecord
661
+ from skyflow.utils.enums import UpsertType
662
+
663
+ vault = skyflow_client.vault('<VAULT_ID>') # skyflow_client from Quickstart
664
+
665
+ request = UpdateRequest(
666
+ table_name='table1',
667
+ update_type=UpsertType.UPDATE,
668
+ records=[UpdateRequestRecord(skyflow_id='9fac9201-7b8a-4446-93f8-5244e1213bd1', data={'cardholder_name': 'jane doe'})],
669
+ )
670
+ response = vault.update(request)
671
+ ```
672
+
673
+ There is no async variant: `update` returns its `UpdateResponse` directly.
674
+
675
+ Sample response:
676
+
677
+ ```json
678
+ {
679
+ "records": [
680
+ {
681
+ "table_name": "table1",
682
+ "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
683
+ "tokens": {
684
+ "cardholder_name": [ { "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "token_group_name": "deterministic_string", "path": null } ]
685
+ },
686
+ "data": { "cardholder_name": "Jane Doe" },
687
+ "hashed_data": null,
688
+ "http_code": 200,
689
+ "error": null,
690
+ "request_id": null
691
+ }
692
+ ]
693
+ }
694
+ ```
695
+
696
+ Like `GetResponseRecord`, `UpdateResponseRecord` carries the insert record's fields plus `.data`: `.table_name`, `.skyflow_id`, `.tokens`, `.data`, `.hashed_data`, `.http_code`, `.error`, `.request_id`. Successes and failures come back in one `records` list, each tagged with its own `.http_code`/`.error`.
697
+
698
+ ```python
699
+ for record in response.records:
700
+ if record.error is None:
701
+ print(record.skyflow_id, 'updated')
702
+ else:
703
+ print(record.skyflow_id, 'failed', record.http_code, record.error)
704
+ ```
705
+
706
+ # Delete
707
+
708
+ Delete records from a table by skyflow ID or unique value, in a single API call.
709
+
710
+ > **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support).
711
+
712
+ **Note:**
713
+
714
+ - `table_name` is required, and exactly one of `ids` or `unique_values` must be supplied — both, or neither, fails validation.
715
+ - `unique_values` takes the same shape as in [Get](#get): one dict per record, holding the unique column-name/value pairs that identify it.
716
+
717
+ ```python
718
+ from skyflow.vault.data import DeleteRequest
719
+
720
+ vault = skyflow_client.vault('<VAULT_ID>') # skyflow_client from Quickstart
721
+
722
+ request = DeleteRequest(
723
+ table_name='table1',
724
+ ids=['9fac9201-7b8a-4446-93f8-5244e1213bd1', 'b2308e2a-c1f5-469b-97b7-1f193159399b'],
725
+ )
726
+ response = vault.delete(request)
727
+ ```
728
+
729
+ There is no async variant: `delete` returns its `DeleteResponse` directly.
730
+
731
+ Sample response:
732
+
733
+ ```json
734
+ {
735
+ "records": [
736
+ { "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "http_code": 200, "error": null, "request_id": null },
737
+ { "skyflow_id": "b2308e2a-c1f5-469b-97b7-1f193159399b", "http_code": 404, "error": "Record Not Found", "request_id": "a1b2c3d4-..." }
738
+ ]
739
+ }
740
+ ```
741
+
742
+ `DeleteResponseRecord` is flatter than the insert-shaped records above — the vault returns no data, tokens, or hashed data for a delete. Accessors: `.skyflow_id`, `.http_code`, `.error`, `.request_id`.
743
+
744
+ ```python
745
+ for record in response.records:
746
+ if record.error is None:
747
+ print(record.skyflow_id, 'deleted')
748
+ else:
749
+ print(record.skyflow_id, 'failed', record.http_code, record.error)
750
+ ```
751
+
752
+ # Custom Request Headers
753
+
754
+ To include custom HTTP headers on an outgoing request, pass an **interceptor** via that operation's options object. The interceptor is a callable that receives a `RequestContext` and can add headers to it. The headers available are defined by the `CustomHeaderKey` enum:
755
+
756
+ | `CustomHeaderKey` | HTTP header name |
757
+ |---|---|
758
+ | `SKYFLOW_ACCOUNT_ID` | `x-skyflow-account-id` |
759
+ | `SKYFLOW_ACCOUNT_NAME` | `x-skyflow-account-name` |
760
+ | `REQUEST_ID_HEADER` | `x-request-id` |
761
+
762
+ ```python
763
+ from skyflow.vault.data import InsertOptions, CustomHeaderKey
764
+
765
+ def add_request_id(context):
766
+ context.add_header(CustomHeaderKey.REQUEST_ID_HEADER, 'req-001')
767
+
768
+ response = vault.insert(request, InsertOptions(interceptor=add_request_id))
769
+ ```
770
+
771
+ The interceptor runs once per call. Headers it adds are merged on top of the SDK's own headers (metrics + `Authorization`).
772
+
773
+ The same pattern applies to every operation, via its corresponding options class:
774
+
775
+ | Operation | Options class |
776
+ |---|---|
777
+ | `insert` | `InsertOptions` |
778
+ | `detokenize` | `DetokenizeOptions` |
779
+ | `get` | `GetOptions` |
780
+ | `update` | `UpdateOptions` |
781
+ | `delete` | `DeleteOptions` |
782
+
783
+ # Error Handling
784
+
785
+ ## Two layers of errors
786
+
787
+ This is the mental model to hold for every operation:
788
+
789
+ | Layer | What it covers | How you see it |
790
+ |---|---|---|
791
+ | **Request-level** | The call could not be made or the whole call failed: invalid request shape, missing credentials, auth failure, or a whole-call API rejection. | A raised `SkyflowError`. No results at all. |
792
+ | **Record-level** | The call succeeded, but individual records or tokens inside it did not. | A returned response. **Nothing is raised.** Each entry in `response.records` reports its own `http_code` and `error`. |
793
+
794
+ The second layer is what distinguishes `flowvault` from an all-or-nothing API: **a call that returns normally can still contain failures, and a call where every single record failed also returns normally rather than raising.** Checking only for a raised exception will silently miss failed records — always read the per-record results.
795
+
796
+ Each record carries its own `request_id` — `None` on success, the failing call's `x-request-id` on error.
797
+
798
+ ## Per-record success and failure
799
+
800
+ Every response exposes `.records`. The records list has one entry per submitted item, in the order you submitted it, and each entry carries:
801
+
802
+ | Attribute | Present on | Meaning |
803
+ |---|---|---|
804
+ | `.http_code` | always | Per-item status. `2xx` for success; `4xx`/`5xx` for failure. |
805
+ | `.error` | always | Error message for this item, populated only on failure — `None` means this item succeeded. |
806
+ | `.request_id` | always | The `x-request-id` of the call this item was part of, populated only on failure (`None` on success) — quote it in support escalations. |
807
+
808
+ The success payload sits alongside those attributes on the same object: `.skyflow_id`/`.tokens`/`.data`/`.hashed_data` for record-shaped operations (`insert`, `get`, `update`), `.value`/`.token_group_name`/`.metadata` for detokenize, `.skyflow_id` alone for delete.
809
+
810
+ A response has just `.records`, in submitted order, with `.http_code`, `.error`, and `.request_id` on each entry alongside that operation's payload.
811
+
812
+ The idiomatic way to consume a response:
813
+
814
+ ```python
815
+ response = vault.insert(request)
816
+
817
+ for record in response.records:
818
+ if record.error is None:
819
+ print(record.skyflow_id)
820
+ else:
821
+ print('failed', record.http_code, record.error, '(request_id', record.request_id, ')')
822
+ ```
823
+
824
+ ## Catching SkyflowError
825
+
826
+ `SkyflowError` covers the request-level layer only — client-side validation errors and whole-call API errors. It comes from `common`, so it is the same exception type `skyvault` raises.
827
+
828
+ ```python
829
+ from skyflow.error import SkyflowError
830
+
831
+ try:
832
+ response = vault.insert(request)
833
+ # reaching here means the CALL succeeded — individual records may still have failed
834
+ except SkyflowError as e:
835
+ print('HTTP code :', e.http_code)
836
+ print('Message :', e.message)
837
+ print('Request ID:', e.request_id)
838
+ print('Details :', e.details)
839
+ except Exception as e:
840
+ print('Unexpected error:', e)
841
+ ```
842
+
843
+ ## SkyflowError properties
844
+
845
+ | Property | Attribute | Description |
846
+ |---|---|---|
847
+ | HTTP status code | `.http_code` | Integer status code (e.g. `400`, `404`, `500`). |
848
+ | Message | `.message` | Human-readable description of the error. |
849
+ | gRPC code | `.grpc_code` | gRPC status code from the server. |
850
+ | HTTP status string | `.http_status` | Status string from the server. |
851
+ | Request ID | `.request_id` | The `x-request-id` header — useful for support escalations. |
852
+ | Details | `.details` | Additional error context from the server. Empty for validation errors, `None` if the server response omitted the field. |
853
+
854
+ **Validation errors** (table name at the wrong level, empty token list, and similar) are raised before any network call, with `http_code` `400`. **API errors** are returned by the Skyflow server and have all fields populated from the response body and headers.
855
+
856
+ ## Retrying the failed records
857
+
858
+ Because failures are reported per record, a partial failure can be retried without resubmitting the whole payload. Resend only the records worth retrying — **server-side failures (HTTP 500–599), excluding 529**, which is a permanent capacity-limit code. Client-side (`4xx`) failures are deliberately excluded — those need a fix to the data, not a retry. This is separate from the transport-level `max_retries` setting in [Timeouts and retries](#timeouts-and-retries), which retries whole HTTP attempts and is off by default.
859
+
860
+ Records come back in input order, so you can filter `response.records` yourself with this predicate:
861
+
862
+ ```python
863
+ def is_retryable(http_code):
864
+ return isinstance(http_code, int) and 500 <= http_code <= 599 and http_code != 529
865
+ ```
866
+
867
+ For `insert` — and likewise `update`, `get`, and `delete` — correlate each response record back to your input by position, then resubmit the retryable ones:
868
+
869
+ ```python
870
+ request = InsertRequest(table_name='cards', records=[...])
871
+ response = vault.insert(request)
872
+
873
+ retryable = [
874
+ request.records[i]
875
+ for i, record in enumerate(response.records)
876
+ if is_retryable(record.http_code)
877
+ ]
878
+ if retryable:
879
+ retry_response = vault.insert(InsertRequest(table_name='cards', records=retryable))
880
+ ```
881
+
882
+ > **Make insert retries idempotent.** A record can succeed server-side even when the response never reaches you — a dropped connection, or a `5xx` returned after the row was already written — so a blind resubmit can create a duplicate. Guard against it with `upsert` on a unique column, kept at the same level as `table_name`: a retried record then updates the existing row instead of inserting a second one.
883
+
884
+ For `detokenize`, each response record carries its own `.token`, so filter on that directly — no position bookkeeping needed:
885
+
886
+ ```python
887
+ response = vault.detokenize(request)
888
+
889
+ retry_tokens = [record.token for record in response.records if is_retryable(record.http_code)]
890
+ if retry_tokens:
891
+ retry_response = vault.detokenize(DetokenizeRequest(tokens=retry_tokens))
892
+ ```
893
+
894
+ # Samples
895
+
896
+ Runnable examples live in [samples/](samples/) — one file per operation, plus custom-header, timeout/retry, and service-account examples. See [samples/README.md](samples/README.md) to run them.