Skip to content

Commit 773bdbe

Browse files
SK-1889: Add request ids for partial error cases (#169)
* SK-1889: handle partial error cases
1 parent b98aab0 commit 773bdbe

File tree

7 files changed

+52
-31
lines changed

7 files changed

+52
-31
lines changed

skyflow/generated/rest/api/tokens_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def record_service_detokenize_with_http_info(
174174

175175
_response_types_map: Dict[str, Optional[str]] = {
176176
'200': "V1DetokenizeResponse",
177+
'207': "V1DetokenizeResponse",
177178
'404': "object",
178179
}
179180
response_data = self.api_client.call_api(

skyflow/utils/_utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from urllib.parse import quote
1313
from skyflow.error import SkyflowError
1414
from skyflow.generated.rest import V1UpdateRecordResponse, V1BulkDeleteRecordResponse, \
15-
V1DetokenizeResponse, V1TokenizeResponse, V1GetQueryResponse, V1BulkGetRecordResponse
15+
V1DetokenizeResponse, V1TokenizeResponse, V1GetQueryResponse, V1BulkGetRecordResponse, ApiResponse
1616
from skyflow.utils.logger import log_error, log_error_log
1717
from . import SkyflowMessages, SDK_VERSION
1818
from .enums import Env, ContentType, EnvUrls
@@ -61,7 +61,7 @@ def get_vault_url(cluster_id, env,vault_id, logger = None):
6161
raise SkyflowError(SkyflowMessages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code)
6262

6363
base_url = EnvUrls[env.name].value
64-
protocol = "https" if env != Env.PROD else "http"
64+
protocol = "https"
6565

6666
return f"{protocol}://{cluster_id}.{base_url}"
6767

@@ -195,7 +195,8 @@ def parse_insert_response(api_response, continue_on_error):
195195
errors = []
196196
insert_response = InsertResponse()
197197
if continue_on_error:
198-
for idx, response in enumerate(api_response.responses):
198+
response_data = json.loads(api_response.raw_data.decode('utf-8'))
199+
for idx, response in enumerate(response_data.get('responses', [])):
199200
if response['Status'] == 200:
200201
body = response['Body']
201202
if 'records' in body:
@@ -210,6 +211,7 @@ def parse_insert_response(api_response, continue_on_error):
210211
inserted_fields.append(inserted_field)
211212
elif response['Status'] == 400:
212213
error = {
214+
'request_id': api_response.headers.get('x-request-id'),
213215
'request_index': idx,
214216
'error': response['Body']['error']
215217
}
@@ -264,13 +266,14 @@ def parse_get_response(api_response: V1BulkGetRecordResponse):
264266

265267
return get_response
266268

267-
def parse_detokenize_response(api_response: V1DetokenizeResponse):
269+
def parse_detokenize_response(api_response: ApiResponse[V1DetokenizeResponse]):
268270
detokenized_fields = []
269271
errors = []
270272

271-
for record in api_response.records:
273+
for record in api_response.data.records:
272274
if record.error:
273275
errors.append({
276+
"request_id": api_response.headers.get('x-request-id'),
274277
"token": record.token,
275278
"error": record.error
276279
})

skyflow/utils/enums/env.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from enum import Enum
22

33
class Env(Enum):
4-
DEV = 'DEV',
5-
SANDBOX = 'SANDBOX',
4+
DEV = 'DEV'
5+
SANDBOX = 'SANDBOX'
66
PROD = 'PROD'
77
STAGE = 'STAGE'
88

99
class EnvUrls(Enum):
10-
PROD = "vault.skyflowapis.com",
11-
SANDBOX = "vault.skyflowapis-preview.com",
10+
PROD = "vault.skyflowapis.com"
11+
SANDBOX = "vault.skyflowapis-preview.com"
1212
DEV = "vault.skyflowapis.dev"
1313
STAGE = "vault.skyflowapis.tech"

skyflow/utils/validations/_validations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,15 +514,15 @@ def validate_detokenize_request(logger, request):
514514
raise SkyflowError(SkyflowMessages.Error.EMPTY_TOKENS_LIST_VALUE.value, invalid_input_error_code)
515515

516516
for item in request.data:
517-
if 'token' not in item or 'redaction' not in item:
517+
if 'token' not in item:
518518
raise SkyflowError(SkyflowMessages.Error.INVALID_TOKENS_LIST_VALUE.value(type(request.data)), invalid_input_error_code)
519519
token = item.get('token')
520520
redaction = item.get('redaction')
521521

522522
if not isinstance(token, str) or not token:
523523
raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_TYPE.value.format("DETOKENIZE"), invalid_input_error_code)
524524

525-
if not isinstance(redaction, RedactionType) or not redaction:
525+
if redaction is not None and not isinstance(redaction, RedactionType):
526526
raise SkyflowError(SkyflowMessages.Error.INVALID_REDACTION_TYPE.value.format(type(redaction)), invalid_input_error_code)
527527

528528
def validate_tokenize_request(logger, request):

skyflow/vault/controller/_vault.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from skyflow.utils import SkyflowMessages, parse_insert_response, \
77
handle_exception, parse_update_record_response, parse_delete_response, parse_detokenize_response, \
88
parse_tokenize_response, parse_query_response, parse_get_response, encode_column_values
9+
from skyflow.utils.enums import RedactionType
910
from skyflow.utils.logger import log_info, log_error_log
1011
from skyflow.utils.validations import validate_insert_request, validate_delete_request, validate_query_request, \
1112
validate_get_request, validate_update_request, validate_detokenize_request, validate_tokenize_request
@@ -89,7 +90,7 @@ def insert(self, request: InsertRequest):
8990
log_info(SkyflowMessages.Info.INSERT_TRIGGERED.value, self.__vault_client.get_logger())
9091

9192
if request.continue_on_error:
92-
api_response = records_api.record_service_batch_operation(self.__vault_client.get_vault_id(),
93+
api_response = records_api.record_service_batch_operation_with_http_info(self.__vault_client.get_vault_id(),
9394
insert_body)
9495

9596
else:
@@ -230,14 +231,17 @@ def detokenize(self, request: DetokenizeRequest):
230231
log_info(SkyflowMessages.Info.DETOKENIZE_REQUEST_RESOLVED.value, self.__vault_client.get_logger())
231232
self.__initialize()
232233
tokens_list = [
233-
V1DetokenizeRecordRequest(token=item.get('token'), redaction=item.get('redaction').value)
234+
V1DetokenizeRecordRequest(
235+
token=item.get('token'),
236+
redaction=item.get('redaction').value if item.get('redaction') else RedactionType.DEFAULT.value
237+
)
234238
for item in request.data
235239
]
236240
payload = V1DetokenizePayload(detokenization_parameters=tokens_list, continue_on_error=request.continue_on_error)
237241
tokens_api = self.__vault_client.get_tokens_api()
238242
try:
239243
log_info(SkyflowMessages.Info.DETOKENIZE_TRIGGERED.value, self.__vault_client.get_logger())
240-
api_response = tokens_api.record_service_detokenize(
244+
api_response = tokens_api.record_service_detokenize_with_http_info(
241245
self.__vault_client.get_vault_id(),
242246
detokenize_payload=payload
243247
)

tests/utils/test__utils.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,23 @@ def test_construct_invoke_connection_request_with_form_date_content_type(self):
183183

184184
def test_parse_insert_response(self):
185185
api_response = Mock()
186-
api_response.responses = [
187-
{"Status": 200, "Body": {"records": [{"skyflow_id": "id1"}]}},
188-
{"Status": 400, "Body": {"error": TEST_ERROR_MESSAGE}}
189-
]
186+
187+
api_response.raw_data = json.dumps({
188+
"responses": [
189+
{"Status": 200, "Body": {"records": [{"skyflow_id": "id1"}]}},
190+
{"Status": 400, "Body": {"error": "TEST_ERROR_MESSAGE"}}
191+
]
192+
}).encode('utf-8')
193+
194+
api_response.headers = {"x-request-id": "test-request-id"}
195+
190196
result = parse_insert_response(api_response, continue_on_error=True)
197+
191198
self.assertEqual(len(result.inserted_fields), 1)
192199
self.assertEqual(len(result.errors), 1)
200+
self.assertEqual(result.inserted_fields[0]['skyflow_id'], "id1")
201+
self.assertEqual(result.errors[0]['error'], "TEST_ERROR_MESSAGE")
202+
self.assertEqual(result.errors[0]['request_id'], "test-request-id")
193203

194204
def test_parse_insert_response_continue_on_error_false(self):
195205
mock_api_response = Mock()
@@ -252,11 +262,13 @@ def test_parse_get_response_successful(self):
252262

253263
def test_parse_detokenize_response_with_mixed_records(self):
254264
mock_api_response = Mock()
255-
mock_api_response.records = [
265+
mock_api_response.data = Mock() # Ensure `data` exists
266+
mock_api_response.data.records = [
256267
Mock(token="token1", value="value1", value_type=Mock(value="Type1"), error=None),
257268
Mock(token="token2", value=None, value_type=None, error="Some error"),
258269
Mock(token="token3", value="value3", value_type=Mock(value="Type2"), error=None),
259270
]
271+
mock_api_response.headers = {"x-request-id": "test-request-id"} # Mock headers
260272

261273
result = parse_detokenize_response(mock_api_response)
262274
self.assertIsInstance(result, DetokenizeResponse)
@@ -267,7 +279,7 @@ def test_parse_detokenize_response_with_mixed_records(self):
267279
]
268280

269281
expected_errors = [
270-
{"token": "token2", "error": "Some error"}
282+
{"request_id": "test-request-id", "token": "token2", "error": "Some error"}
271283
]
272284

273285
self.assertEqual(result.detokenized_fields, expected_detokenized_fields)
@@ -415,4 +427,4 @@ def test_encode_column_values(self):
415427
]
416428

417429
result = encode_column_values(get_request)
418-
self.assertEqual(result, expected_encoded_values)
430+
self.assertEqual(result, expected_encoded_values)

tests/vault/controller/test__vault.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from unittest.mock import Mock, patch
33
from skyflow.generated.rest import RecordServiceBatchOperationBody, V1BatchRecord, RecordServiceInsertRecordBody, \
44
V1FieldRecords, RecordServiceUpdateRecordBody, RecordServiceBulkDeleteRecordBody, QueryServiceExecuteQueryBody, \
5-
V1DetokenizeRecordRequest, V1DetokenizePayload, V1TokenizePayload, V1TokenizeRecordRequest, RedactionEnumREDACTION
5+
V1DetokenizeRecordRequest, V1DetokenizePayload, V1TokenizePayload, V1TokenizeRecordRequest, RedactionEnumREDACTION, \
6+
BatchRecordMethod
67
from skyflow.utils.enums import RedactionType, TokenMode
78
from skyflow.vault.controller import Vault
89
from skyflow.vault.data import InsertRequest, InsertResponse, UpdateResponse, UpdateRequest, DeleteResponse, \
@@ -43,7 +44,7 @@ def test_insert_with_continue_on_error(self, mock_parse_response, mock_validate)
4344
V1BatchRecord(
4445
fields={"field": "value"},
4546
table_name=TABLE_NAME,
46-
method="POST",
47+
method=BatchRecordMethod.POST,
4748
tokenization=True,
4849
upsert="column_name"
4950
)
@@ -71,14 +72,14 @@ def test_insert_with_continue_on_error(self, mock_parse_response, mock_validate)
7172
# Set the return value for the parse response
7273
mock_parse_response.return_value = expected_response
7374
records_api = self.vault_client.get_records_api.return_value
74-
records_api.record_service_batch_operation.return_value = mock_api_response
75+
records_api.record_service_batch_operation_with_http_info.return_value = mock_api_response
7576

7677
# Call the insert function
7778
result = self.vault.insert(request)
7879

7980
# Assertions
8081
mock_validate.assert_called_once_with(self.vault_client.get_logger(), request)
81-
records_api.record_service_batch_operation.assert_called_once_with(VAULT_ID, expected_body)
82+
records_api.record_service_batch_operation_with_http_info.assert_called_once_with(VAULT_ID, expected_body)
8283
mock_parse_response.assert_called_once_with(mock_api_response, True)
8384

8485
# Assert that the result matches the expected InsertResponse
@@ -481,28 +482,28 @@ def test_detokenize_successful(self, mock_parse_response, mock_validate):
481482
# Mock API response
482483
mock_api_response = Mock()
483484
mock_api_response.records = [
484-
Mock(token="token1", value="value1", value_type=Mock(value="STRING"), error=None),
485-
Mock(token="token2", value="value2", value_type=Mock(value="STRING"), error=None)
485+
Mock(skyflow_id="id_1", token="token1", value="value1", value_type=Mock(value="STRING"), error=None),
486+
Mock(skyflow_id="id_2", token="token2", value="value2", value_type=Mock(value="STRING"), error=None)
486487
]
487488

488489
# Expected parsed response
489490
expected_fields = [
490-
{"token": "token1", "value": "value1", "type": "STRING"},
491-
{"token": "token2", "value": "value2", "type": "STRING"}
491+
{"skyflow_id": "id_1", "token": "token1", "value": "value1", "type": "STRING"},
492+
{"skyflow_id": "id_2", "token": "token2", "value": "value2", "type": "STRING"}
492493
]
493494
expected_response = DetokenizeResponse(detokenized_fields=expected_fields, errors=[])
494495

495496
# Set the return value for parse_detokenize_response
496497
mock_parse_response.return_value = expected_response
497498
tokens_api = self.vault_client.get_tokens_api.return_value
498-
tokens_api.record_service_detokenize.return_value = mock_api_response
499+
tokens_api.record_service_detokenize_with_http_info.return_value = mock_api_response
499500

500501
# Call the detokenize function
501502
result = self.vault.detokenize(request)
502503

503504
# Assertions
504505
mock_validate.assert_called_once_with(self.vault_client.get_logger(), request)
505-
tokens_api.record_service_detokenize.assert_called_once_with(
506+
tokens_api.record_service_detokenize_with_http_info.assert_called_once_with(
506507
VAULT_ID,
507508
detokenize_payload=expected_payload
508509
)

0 commit comments

Comments
 (0)