From 89d91ad8469c4d1e1667b81c667907e9c08c516d Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Mon, 18 May 2026 17:27:19 +0530 Subject: [PATCH 1/6] SK-2833: add deprecation warnings --- skyflow/client/skyflow.py | 9 ++ skyflow/utils/_skyflow_messages.py | 9 ++ skyflow/vault/data/_file_upload_request.py | 24 ++++- tests/client/test_skyflow.py | 120 +++++++++++++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) diff --git a/skyflow/client/skyflow.py b/skyflow/client/skyflow.py index 7b405f1..d094451 100644 --- a/skyflow/client/skyflow.py +++ b/skyflow/client/skyflow.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from typing_extensions import deprecated from skyflow import LogLevel from skyflow.error import SkyflowError from skyflow.utils import SkyflowMessages @@ -59,6 +60,10 @@ def set_log_level(self, log_level): self.__builder._Builder__set_log_level(log_level) return self + @deprecated("[DEPRECATED] Use set_log_level() instead.") + def update_log_level(self, log_level): + return self.set_log_level(log_level) + def get_log_level(self): return self.__builder._Builder__log_level @@ -111,6 +116,8 @@ def remove_vault_config(self, vault_id): def update_vault_config(self, config): validate_update_vault_config(self.__logger, config) vault_id = config.get(OptionField.VAULT_ID) + if vault_id not in self.__vault_configs: + raise SkyflowError(SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(vault_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) vault_config = self.__vault_configs[vault_id] vault_config.get(OptionField.VAULT_CLIENT).update_config(config) @@ -152,6 +159,8 @@ def remove_connection_config(self, connection_id): def update_connection_config(self, config): validate_update_connection_config(self.__logger, config) connection_id = config[OptionField.CONNECTION_ID] + if connection_id not in self.__connection_configs: + raise SkyflowError(SkyflowMessages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(connection_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) connection_config = self.__connection_configs[connection_id] connection_config.get(OptionField.VAULT_CLIENT).update_config(config) diff --git a/skyflow/utils/_skyflow_messages.py b/skyflow/utils/_skyflow_messages.py index 79f4c7e..a082f97 100644 --- a/skyflow/utils/_skyflow_messages.py +++ b/skyflow/utils/_skyflow_messages.py @@ -414,6 +414,15 @@ class HttpStatus(Enum): class Warning(Enum): WARNING_MESSAGE = "WARNING MESSAGE" + UPDATE_LOG_LEVEL_DEPRECATED = ( + "[DEPRECATED] Skyflow.update_log_level() is deprecated. " + "Use Skyflow.set_log_level() instead — identical behavior." + ) + FILE_UPLOAD_REQUEST_ARG_ORDER_DEPRECATED = ( + "[DEPRECATED] FileUploadRequest: argument order changed. " + "Old positional order: (table, skyflow_id, column_name). " + "New order: FileUploadRequest(table, column_name=..., skyflow_id=...)." + ) diff --git a/skyflow/vault/data/_file_upload_request.py b/skyflow/vault/data/_file_upload_request.py index 2f82c3e..ed56099 100644 --- a/skyflow/vault/data/_file_upload_request.py +++ b/skyflow/vault/data/_file_upload_request.py @@ -1,14 +1,28 @@ +import warnings from typing import BinaryIO, Optional +from skyflow.utils import SkyflowMessages + + class FileUploadRequest: def __init__(self, table: str, - column_name: str, + *args, + column_name: Optional[str] = None, skyflow_id: Optional[str] = None, - file_path: str= None, - base64: str= None, - file_object: BinaryIO= None, - file_name: str= None): + file_path: Optional[str] = None, + base64: Optional[str] = None, + file_object: Optional[BinaryIO] = None, + file_name: Optional[str] = None): + if args: + warnings.warn( + SkyflowMessages.Warning.FILE_UPLOAD_REQUEST_ARG_ORDER_DEPRECATED.value, + DeprecationWarning, + stacklevel=2, + ) + # Old positional order was: (table, skyflow_id, column_name, ...) + skyflow_id = args[0] if len(args) >= 1 else skyflow_id + column_name = args[1] if len(args) >= 2 else column_name self.table = table self.skyflow_id = skyflow_id self.column_name = column_name diff --git a/tests/client/test_skyflow.py b/tests/client/test_skyflow.py index dcf80f1..cf9d541 100644 --- a/tests/client/test_skyflow.py +++ b/tests/client/test_skyflow.py @@ -1,4 +1,5 @@ import unittest +import warnings from unittest.mock import patch, Mock from skyflow import LogLevel, Env @@ -6,6 +7,7 @@ from skyflow.utils import SkyflowMessages from skyflow import Skyflow from skyflow.vault.client.client import VaultClient +from skyflow.vault.data import FileUploadRequest VALID_VAULT_CONFIG = { "vault_id": "VAULT_ID", @@ -342,6 +344,51 @@ def test_skyflow_vault_and_connection_method(self, mock_get_vault_config): skyflow_client.connection() mock_get_vault_config.assert_called_once() + def test_detect_returns_detect_controller(self): + skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + from skyflow.vault.controller import Detect + result = skyflow_client.detect() + self.assertIsInstance(result, Detect) + + def test_detect_with_explicit_vault_id(self): + skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + from skyflow.vault.controller import Detect + result = skyflow_client.detect(VALID_VAULT_CONFIG["vault_id"]) + self.assertIsInstance(result, Detect) + + def test_detect_with_invalid_vault_id_raises_error(self): + skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + with self.assertRaises(SkyflowError) as context: + skyflow_client.detect("invalid_vault_id") + self.assertEqual( + context.exception.message, + SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format("invalid_vault_id"), + ) + + @patch("skyflow.vault.client.client.VaultClient.update_config") + def test_update_vault_config_with_invalid_vault_id_raises_error(self, _mock): + skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + invalid_config = VALID_VAULT_CONFIG.copy() + invalid_config["vault_id"] = "non_existent_vault_id" + with self.assertRaises(SkyflowError) as context: + skyflow_client.update_vault_config(invalid_config) + self.assertEqual( + context.exception.message, + SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format("non_existent_vault_id"), + ) + + @patch("skyflow.vault.client.client.VaultClient.update_config") + def test_update_connection_config_with_invalid_connection_id_raises_error(self, _mock): + skyflow_client = self.builder.add_connection_config(VALID_CONNECTION_CONFIG).build() + invalid_config = VALID_CONNECTION_CONFIG.copy() + invalid_config["connection_id"] = "non_existent_connection_id" + with self.assertRaises(SkyflowError) as context: + skyflow_client.update_connection_config(invalid_config) + self.assertEqual( + context.exception.message, + SkyflowMessages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format("non_existent_connection_id"), + ) + class TestVaultClient(unittest.TestCase): def _make_client(self): @@ -372,3 +419,76 @@ def test_get_bearer_token_passes_token_uri_option(self, _mock_expired, mock_gen) options_passed = mock_gen.call_args[0][1] self.assertIn("token_uri", options_passed) self.assertEqual(options_passed["token_uri"], "https://custom-token-uri.com/token") + + +class TestUpdateLogLevelDeprecation(unittest.TestCase): + def _build_client(self): + return Skyflow.builder().add_vault_config(VALID_VAULT_CONFIG).build() + + def test_update_log_level_emits_deprecation_warning(self): + client = self._build_client() + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + client.update_log_level(LogLevel.INFO) + self.assertEqual(len(caught), 1) + self.assertTrue(issubclass(caught[0].category, DeprecationWarning)) + self.assertIn("set_log_level", str(caught[0].message)) + + def test_update_log_level_warning_points_at_caller(self): + client = self._build_client() + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + client.update_log_level(LogLevel.INFO) + self.assertEqual(caught[0].filename, __file__) + + def test_update_log_level_delegates_to_set_log_level(self): + client = self._build_client() + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + client.update_log_level(LogLevel.INFO) + self.assertEqual(client.get_log_level(), LogLevel.INFO) + + +class TestFileUploadRequestDeprecation(unittest.TestCase): + def test_keyword_args_no_warning(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + req = FileUploadRequest( + table="table", + column_name="col", + skyflow_id="sky123", + ) + self.assertEqual(len(caught), 0) + self.assertEqual(req.table, "table") + self.assertEqual(req.column_name, "col") + self.assertEqual(req.skyflow_id, "sky123") + + def test_old_positional_order_emits_deprecation_warning(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + req = FileUploadRequest("table", "sky123", "col") + self.assertEqual(len(caught), 1) + self.assertTrue(issubclass(caught[0].category, DeprecationWarning)) + self.assertIn("FileUploadRequest", str(caught[0].message)) + + def test_old_positional_order_remaps_args_correctly(self): + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + req = FileUploadRequest("table", "sky123", "col") + self.assertEqual(req.skyflow_id, "sky123") + self.assertEqual(req.column_name, "col") + + def test_old_positional_order_warning_points_at_caller(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + FileUploadRequest("table", "sky123", "col") + self.assertEqual(caught[0].filename, __file__) + + def test_single_positional_arg_emits_warning_and_sets_skyflow_id(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + req = FileUploadRequest("table", "sky123") + self.assertEqual(len(caught), 1) + self.assertTrue(issubclass(caught[0].category, DeprecationWarning)) + self.assertEqual(req.skyflow_id, "sky123") + self.assertIsNone(req.column_name) From ed4956a1281f7247947299d6c4770e032a9d411a Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 May 2026 01:09:55 +0530 Subject: [PATCH 2/6] SK-2833: update file upload request --- skyflow/vault/data/_file_upload_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyflow/vault/data/_file_upload_request.py b/skyflow/vault/data/_file_upload_request.py index ed56099..6a632b6 100644 --- a/skyflow/vault/data/_file_upload_request.py +++ b/skyflow/vault/data/_file_upload_request.py @@ -21,8 +21,8 @@ def __init__(self, stacklevel=2, ) # Old positional order was: (table, skyflow_id, column_name, ...) - skyflow_id = args[0] if len(args) >= 1 else skyflow_id - column_name = args[1] if len(args) >= 2 else column_name + skyflow_id = args[0] if args else skyflow_id + column_name = args[1] if len(args) > 1 else column_name self.table = table self.skyflow_id = skyflow_id self.column_name = column_name From 0d08bbadf184eb204d99d68b3d17640d7d6c1553 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 May 2026 12:54:46 +0530 Subject: [PATCH 3/6] SK-2838: update CHANGELOG.md with latest releases --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f56451..f63ab2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,59 @@ All notable changes to this project will be documented in this file. +## [2.0.2] - 2026-05-06 +### Added +- Dict context support for Conditional Data Access. + +## [2.0.1] - 2026-04-29 +### Fixed +- Fern client re-initialisation on token refresh. + +## [2.0.0] - 2025-11-11 +### Added +- Multi-vault and multi-connection support via fluent builder (`Skyflow.builder()`). +- New typed request and response classes for all vault operations (`InsertRequest`, `GetRequest`, `UpdateRequest`, `DeleteRequest`, `QueryRequest`, `DetokenizeRequest`, `TokenizeRequest`, `FileUploadRequest`). +- Detect API: `deidentify_text`, `reidentify_text`, `deidentify_file`, and `get_detect_run`. +- File upload support via `vault().upload_file()`. +- Flexible credential types: API key, static bearer token, service account credentials string, credentials file path, and `SKYFLOW_CREDENTIALS` environment variable. +- `SkyflowError` now includes `http_code`, `grpc_code`, `http_status`, `request_id`, and `details` fields. +- `set_log_level()` on the client for runtime log level changes. + +### Changed +- Complete rewrite of the SDK public API. See [docs/migrate_to_v2.md](docs/migrate_to_v2.md) for migration instructions. + +## [1.16.0] - 2025-09-23 +### Fixed +- Remote disconnect error in vault operations. + +## [1.15.8] - 2025-09-30 +### Fixed +- Retry logic when `continue_on_error` is set to `true` in insert. + +## [1.15.7] - 2025-09-23 +### Fixed +- Retry handling for errors in insert method. + +## [1.15.6] - 2025-09-22 +### Fixed +- Added retry logic for transient errors. + +## [1.15.5] - 2025-09-18 +### Fixed +- Remote disconnected errors in vault operations. + +## [1.15.4] - 2025-09-12 +### Fixed +- Retry on exception during vault requests. + +## [1.15.3] - 2025-09-12 +### Fixed +- Retry on exception during vault requests. + +## [1.15.2] - 2025-09-12 +### Fixed +- Retry on connection error in insert method. + ## [1.15.1] - 2023-12-07 ## Fixed - Not receiving tokens when calling Get with options tokens as true. From 151ee887c7e575844defa0898cb1c47fd78d98f9 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 May 2026 15:55:20 +0530 Subject: [PATCH 4/6] SK-2833: fix claude identified issues --- skyflow/client/skyflow.py | 6 ++ skyflow/error/_skyflow_error.py | 4 +- skyflow/service_account/_utils.py | 31 +++++---- skyflow/utils/_skyflow_messages.py | 3 +- skyflow/utils/validations/_validations.py | 67 +++++++++++++++---- .../vault/detect/_deidentify_file_response.py | 26 +++---- tests/client/test_skyflow.py | 10 +-- tests/utils/test__utils.py | 2 +- 8 files changed, 101 insertions(+), 48 deletions(-) diff --git a/skyflow/client/skyflow.py b/skyflow/client/skyflow.py index d094451..2255ee5 100644 --- a/skyflow/client/skyflow.py +++ b/skyflow/client/skyflow.py @@ -1,3 +1,4 @@ +import warnings from collections import OrderedDict from typing_extensions import deprecated from skyflow import LogLevel @@ -62,6 +63,11 @@ def set_log_level(self, log_level): @deprecated("[DEPRECATED] Use set_log_level() instead.") def update_log_level(self, log_level): + warnings.warn( + SkyflowMessages.Warning.UPDATE_LOG_LEVEL_DEPRECATED.value, + DeprecationWarning, + stacklevel=2, + ) return self.set_log_level(log_level) def get_log_level(self): diff --git a/skyflow/error/_skyflow_error.py b/skyflow/error/_skyflow_error.py index 80a959d..0de8640 100644 --- a/skyflow/error/_skyflow_error.py +++ b/skyflow/error/_skyflow_error.py @@ -7,11 +7,11 @@ def __init__(self, request_id = None, grpc_code = None, http_status = None, - details = []): + details = None): self.message = message self.http_code = http_code self.grpc_code = grpc_code self.http_status = http_status if http_status else SkyflowMessages.HttpStatus.BAD_REQUEST.value - self.details = details + self.details = details if details is not None else [] self.request_id = request_id super().__init__(message) \ No newline at end of file diff --git a/skyflow/service_account/_utils.py b/skyflow/service_account/_utils.py index 4d3a457..deccf97 100644 --- a/skyflow/service_account/_utils.py +++ b/skyflow/service_account/_utils.py @@ -82,15 +82,16 @@ def is_expired(token, logger = None): def generate_bearer_token(credentials_file_path, options = None, logger = None): log_info(SkyflowMessages.Info.GET_BEARER_TOKEN_TRIGGERED.value, logger) try: - credentials_file = open(credentials_file_path, 'r') + with open(credentials_file_path, 'r') as credentials_file: + try: + credentials = json.load(credentials_file) + except Exception: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_CREDENTIALS_FILE.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), invalid_input_error_code) + except SkyflowError: + raise except Exception: raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH.value, invalid_input_error_code) - with credentials_file: - try: - credentials = json.load(credentials_file) - except Exception: - log_error_log(SkyflowMessages.ErrorLogs.INVALID_CREDENTIALS_FILE.value, logger=logger) - raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), invalid_input_error_code) result = get_service_account_token(credentials, options, logger) return result @@ -179,6 +180,7 @@ def get_signed_jwt(options, client_id, key_id, token_uri, private_key, logger): def get_signed_tokens(credentials_obj, options): + options = options if options is not None else {} credentials_obj = _normalize_credentials(credentials_obj) expiry_time = int(time.time()) + options.get(OptionField.TIME_TO_LIVE, 60) prefix = JWT.SIGNED_TOKEN_PREFIX @@ -218,15 +220,16 @@ def get_signed_tokens(credentials_obj, options): def generate_signed_data_tokens(credentials_file_path, options): log_info(SkyflowMessages.Info.GET_SIGNED_DATA_TOKENS_TRIGGERED.value) try: - credentials_file = open(credentials_file_path, 'r') + with open(credentials_file_path, 'r') as credentials_file: + try: + credentials = json.load(credentials_file) + except Exception: + raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), + invalid_input_error_code) + except SkyflowError: + raise except Exception: raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH.value, invalid_input_error_code) - with credentials_file: - try: - credentials = json.load(credentials_file) - except Exception: - raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), - invalid_input_error_code) return get_signed_tokens(credentials, options) def generate_signed_data_tokens_from_creds(credentials, options): diff --git a/skyflow/utils/_skyflow_messages.py b/skyflow/utils/_skyflow_messages.py index a082f97..1acfcde 100644 --- a/skyflow/utils/_skyflow_messages.py +++ b/skyflow/utils/_skyflow_messages.py @@ -317,6 +317,8 @@ class Info(Enum): DETECT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Detect request is resolved." class ErrorLogs(Enum): + INVALID_LOG_LEVEL = f"{ERROR}: [{error_prefix}] Invalid log level. Specify a valid log level." + INVALID_KEY = f"{ERROR}: [{error_prefix}] Invalid key {{}} in config." VAULTID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid vault config. Vault ID is required." EMPTY_VAULTID = f"{ERROR}: [{error_prefix}] Invalid vault config. Vault ID can not be empty." CLUSTER_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid vault config. Cluster ID is required." @@ -413,7 +415,6 @@ class HttpStatus(Enum): BAD_REQUEST = "Bad Request" class Warning(Enum): - WARNING_MESSAGE = "WARNING MESSAGE" UPDATE_LOG_LEVEL_DEPRECATED = ( "[DEPRECATED] Skyflow.update_log_level() is deprecated. " "Use Skyflow.set_log_level() instead — identical behavior." diff --git a/skyflow/utils/validations/_validations.py b/skyflow/utils/validations/_validations.py index f07398f..59e24d1 100644 --- a/skyflow/utils/validations/_validations.py +++ b/skyflow/utils/validations/_validations.py @@ -170,15 +170,18 @@ def validate_credentials(logger, credentials, config_id_type=None, config_id=Non or not isinstance(token_uri, str) or not is_valid_url(token_uri) ): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_TOKEN_URI.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) def validate_log_level(logger, log_level): if not isinstance(log_level, LogLevel): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_LOG_LEVEL.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_LOG_LEVEL.value, invalid_input_error_code) def validate_keys(logger, config, config_keys): for key in config.keys(): if key not in config_keys: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_KEY.value.format(key), logger) raise SkyflowError(SkyflowMessages.Error.INVALID_KEY.value.format(key), invalid_input_error_code) def validate_vault_config(logger, config): @@ -290,135 +293,164 @@ def validate_update_connection_config(logger, config): def validate_file_from_request(file_input: FileInput): if file_input is None: + log_error_log(SkyflowMessages.Error.INVALID_FILE_INPUT.value) raise SkyflowError(SkyflowMessages.Error.INVALID_FILE_INPUT.value, invalid_input_error_code) - + has_file = hasattr(file_input, FileUploadField.FILE) and file_input.file is not None has_file_path = hasattr(file_input, FileUploadField.FILE_PATH) and file_input.file_path is not None - + # Must provide exactly one of file or file_path if (has_file and has_file_path) or (not has_file and not has_file_path): + log_error_log(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_INPUT.value) raise SkyflowError(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_INPUT.value, invalid_input_error_code) - + if has_file: file = file_input.file # Validate file object has required attributes if not hasattr(file, FileUploadField.NAME) or not isinstance(file.name, str) or not file.name.strip(): + log_error_log(SkyflowMessages.Error.INVALID_FILE_TYPE.value) raise SkyflowError(SkyflowMessages.Error.INVALID_FILE_TYPE.value, invalid_input_error_code) - + # Validate file name file_name, _ = os.path.splitext(os.path.basename(file.name)) if not file_name or not file_name.strip(): + log_error_log(SkyflowMessages.Error.INVALID_FILE_NAME.value) raise SkyflowError(SkyflowMessages.Error.INVALID_FILE_NAME.value, invalid_input_error_code) - + elif has_file_path: file_path = file_input.file_path if not isinstance(file_path, str) or not file_path.strip(): + log_error_log(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_PATH.value) raise SkyflowError(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_PATH.value, invalid_input_error_code) - + if not os.path.exists(file_path) or not os.path.isfile(file_path): + log_error_log(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_PATH.value) raise SkyflowError(SkyflowMessages.Error.INVALID_DEIDENTIFY_FILE_PATH.value, invalid_input_error_code) def validate_deidentify_file_request(logger, request: DeidentifyFileRequest): if not hasattr(request, FileUploadField.FILE) or request.file is None: + log_error_log(SkyflowMessages.Error.INVALID_FILE_INPUT.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_FILE_INPUT.value, invalid_input_error_code) - + # Validate file input first validate_file_from_request(request.file) # Optional: entities if hasattr(request, DeidentifyFileRequestField.ENTITIES) and request.entities is not None: if not isinstance(request.entities, list): + log_error_log(SkyflowMessages.Error.INVALID_DETECT_ENTITIES_TYPE.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_DETECT_ENTITIES_TYPE.value, invalid_input_error_code) if not all(isinstance(entity, DetectEntities) for entity in request.entities): + log_error_log(SkyflowMessages.Error.INVALID_DETECT_ENTITIES_TYPE.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_DETECT_ENTITIES_TYPE.value, invalid_input_error_code) # Optional: allow_regex_list if hasattr(request, DeidentifyFileRequestField.ALLOW_REGEX_LIST) and request.allow_regex_list is not None: if not isinstance(request.allow_regex_list, list) or not all(isinstance(x, str) for x in request.allow_regex_list): + log_error_log(SkyflowMessages.Error.INVALID_ALLOW_REGEX_LIST.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_ALLOW_REGEX_LIST.value, invalid_input_error_code) # Optional: restrict_regex_list if hasattr(request, DeidentifyFileRequestField.RESTRICT_REGEX_LIST) and request.restrict_regex_list is not None: if not isinstance(request.restrict_regex_list, list) or not all(isinstance(x, str) for x in request.restrict_regex_list): + log_error_log(SkyflowMessages.Error.INVALID_RESTRICT_REGEX_LIST.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_RESTRICT_REGEX_LIST.value, invalid_input_error_code) # Optional: token_format if request.token_format is not None and not isinstance(request.token_format, TokenFormat): + log_error_log(SkyflowMessages.Error.INVALID_TOKEN_FORMAT.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_FORMAT.value, invalid_input_error_code) # Optional: transformations if request.transformations is not None and not isinstance(request.transformations, Transformations): + log_error_log(SkyflowMessages.Error.INVALID_TRANSFORMATIONS.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TRANSFORMATIONS.value, invalid_input_error_code) # Optional: output_processed_image if hasattr(request, DeidentifyFileRequestField.OUTPUT_PROCESSED_IMAGE) and request.output_processed_image is not None: if not isinstance(request.output_processed_image, bool): + log_error_log(SkyflowMessages.Error.INVALID_OUTPUT_PROCESSED_IMAGE.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_OUTPUT_PROCESSED_IMAGE.value, invalid_input_error_code) # Optional: output_ocr_text if hasattr(request, DeidentifyFileRequestField.OUTPUT_OCR_TEXT) and request.output_ocr_text is not None: if not isinstance(request.output_ocr_text, bool): + log_error_log(SkyflowMessages.Error.INVALID_OUTPUT_OCR_TEXT.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_OUTPUT_OCR_TEXT.value, invalid_input_error_code) # Optional: masking_method if hasattr(request, DeidentifyFileRequestField.MASKING_METHOD) and request.masking_method is not None: if not isinstance(request.masking_method, MaskingMethod): + log_error_log(SkyflowMessages.Error.INVALID_MASKING_METHOD.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_MASKING_METHOD.value, invalid_input_error_code) # Optional: pixel_density if hasattr(request, DeidentifyFileRequestField.PIXEL_DENSITY) and request.pixel_density is not None: if not isinstance(request.pixel_density, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_PIXEL_DENSITY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_PIXEL_DENSITY.value, invalid_input_error_code) # Optional: max_resolution if hasattr(request, DeidentifyFileRequestField.MAX_RESOLUTION) and request.max_resolution is not None: if not isinstance(request.max_resolution, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_MAXIMUM_RESOLUTION.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_MAXIMUM_RESOLUTION.value, invalid_input_error_code) # Optional: output_processed_audio if hasattr(request, DeidentifyFileRequestField.OUTPUT_PROCESSED_AUDIO) and request.output_processed_audio is not None: if not isinstance(request.output_processed_audio, bool): + log_error_log(SkyflowMessages.Error.INVALID_OUTPUT_PROCESSED_AUDIO.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_OUTPUT_PROCESSED_AUDIO.value, invalid_input_error_code) # Optional: output_transcription if hasattr(request, DeidentifyFileRequestField.OUTPUT_TRANSCRIPTION) and request.output_transcription is not None: if not isinstance(request.output_transcription, DetectOutputTranscriptions): + log_error_log(SkyflowMessages.Error.INVALID_OUTPUT_TRANSCRIPTION.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_OUTPUT_TRANSCRIPTION.value, invalid_input_error_code) # Optional: bleep if hasattr(request, DeidentifyFileRequestField.BLEEP) and request.bleep is not None: if not isinstance(request.bleep, Bleep): + log_error_log(SkyflowMessages.Error.INVALID_BLEEP_TYPE.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_BLEEP_TYPE.value, invalid_input_error_code) - + # Validate gain if request.bleep.gain is not None and not isinstance(request.bleep.gain, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_BLEEP_GAIN.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_BLEEP_GAIN.value, invalid_input_error_code) - + # Validate frequency if request.bleep.frequency is not None and not isinstance(request.bleep.frequency, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_BLEEP_FREQUENCY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_BLEEP_FREQUENCY.value, invalid_input_error_code) - + # Validate start_padding if request.bleep.start_padding is not None and not isinstance(request.bleep.start_padding, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_BLEEP_START_PADDING.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_BLEEP_START_PADDING.value, invalid_input_error_code) - + # Validate stop_padding if request.bleep.stop_padding is not None and not isinstance(request.bleep.stop_padding, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_BLEEP_STOP_PADDING.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_BLEEP_STOP_PADDING.value, invalid_input_error_code) # Optional: output_directory if hasattr(request, DeidentifyFileRequestField.OUTPUT_DIRECTORY) and request.output_directory is not None: if not isinstance(request.output_directory, str): + log_error_log(SkyflowMessages.Error.INVALID_OUTPUT_DIRECTORY_VALUE.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_OUTPUT_DIRECTORY_VALUE.value, invalid_input_error_code) if not os.path.isdir(request.output_directory): + log_error_log(SkyflowMessages.Error.OUTPUT_DIRECTORY_NOT_FOUND.value.format(request.output_directory), logger) raise SkyflowError(SkyflowMessages.Error.OUTPUT_DIRECTORY_NOT_FOUND.value.format(request.output_directory), invalid_input_error_code) # Optional: wait_time if hasattr(request, DeidentifyFileRequestField.WAIT_TIME) and request.wait_time is not None: if not isinstance(request.wait_time, (int, float)): + log_error_log(SkyflowMessages.Error.INVALID_WAIT_TIME.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_WAIT_TIME.value, invalid_input_error_code) if request.wait_time < 0 or request.wait_time > Detect.WAIT_TIME: + log_error_log(SkyflowMessages.Error.WAIT_TIME_GREATER_THEN_64.value, logger) raise SkyflowError(SkyflowMessages.Error.WAIT_TIME_GREATER_THEN_64.value, invalid_input_error_code) def validate_insert_request(logger, request): @@ -798,44 +830,55 @@ def validate_invoke_connection_params(logger, query_params, path_params): def validate_deidentify_text_request(logger, request: DeidentifyTextRequest): if not request.text or not isinstance(request.text, str) or not request.text.strip(): + log_error_log(SkyflowMessages.Error.INVALID_TEXT_IN_DEIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TEXT_IN_DEIDENTIFY.value, invalid_input_error_code) # Validate entities if present if request.entities is not None and not isinstance(request.entities, list): + log_error_log(SkyflowMessages.Error.INVALID_ENTITIES_IN_DEIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_ENTITIES_IN_DEIDENTIFY.value, invalid_input_error_code) # Validate allowed_regex_list if present if request.allow_regex_list is not None and not isinstance(request.allow_regex_list, list): + log_error_log(SkyflowMessages.Error.INVALID_ALLOW_REGEX_LIST.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_ALLOW_REGEX_LIST.value, invalid_input_error_code) # Validate restricted_regex_list if present if request.restrict_regex_list is not None and not isinstance(request.restrict_regex_list, list): + log_error_log(SkyflowMessages.Error.INVALID_RESTRICT_REGEX_LIST.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_RESTRICT_REGEX_LIST.value, invalid_input_error_code) # Validate token_format if present if request.token_format is not None and not isinstance(request.token_format, TokenFormat): + log_error_log(SkyflowMessages.Error.INVALID_TOKEN_FORMAT.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_FORMAT.value, invalid_input_error_code) # Validate transformations if present if request.transformations is not None and not isinstance(request.transformations, Transformations): + log_error_log(SkyflowMessages.Error.INVALID_TRANSFORMATIONS.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TRANSFORMATIONS.value, invalid_input_error_code) def validate_reidentify_text_request(logger, request: ReidentifyTextRequest): if not request.text or not isinstance(request.text, str) or not request.text.strip(): + log_error_log(SkyflowMessages.Error.INVALID_TEXT_IN_REIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_TEXT_IN_REIDENTIFY.value, invalid_input_error_code) # Validate redacted_entities if present if request.redacted_entities is not None and not isinstance(request.redacted_entities, list): + log_error_log(SkyflowMessages.Error.INVALID_REDACTED_ENTITIES_IN_REIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_REDACTED_ENTITIES_IN_REIDENTIFY.value, invalid_input_error_code) # Validate masked_entities if present if request.masked_entities is not None and not isinstance(request.masked_entities, list): + log_error_log(SkyflowMessages.Error.INVALID_MASKED_ENTITIES_IN_REIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_MASKED_ENTITIES_IN_REIDENTIFY.value, invalid_input_error_code) # Validate plain_text_entities if present if request.plain_text_entities is not None and not isinstance(request.plain_text_entities, list): + log_error_log(SkyflowMessages.Error.INVALID_PLAIN_TEXT_ENTITIES_IN_REIDENTIFY.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_PLAIN_TEXT_ENTITIES_IN_REIDENTIFY.value, invalid_input_error_code) def validate_get_detect_run_request(logger, request: GetDetectRunRequest): - if not request.run_id or not isinstance(request.run_id, str) or not request.run_id.strip(): + if request.run_id is None or not isinstance(request.run_id, str) or not request.run_id.strip(): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_RUN_ID.value, logger) raise SkyflowError(SkyflowMessages.Error.INVALID_RUN_ID.value, invalid_input_error_code) diff --git a/skyflow/vault/detect/_deidentify_file_response.py b/skyflow/vault/detect/_deidentify_file_response.py index 97b8df4..e56f211 100644 --- a/skyflow/vault/detect/_deidentify_file_response.py +++ b/skyflow/vault/detect/_deidentify_file_response.py @@ -5,19 +5,19 @@ class DeidentifyFileResponse: def __init__( self, - file_base64: str = None, - file: io.BytesIO = None, - type: str = None, - extension: str = None, - word_count: int = None, - char_count: int = None, - size_in_kb: float = None, - duration_in_seconds: float = None, - page_count: int = None, - slide_count: int = None, - entities: list = None, # list of dicts with keys 'file' and 'extension' - run_id: str = None, - status: str = None, + file_base64: Optional[str] = None, + file: Optional[io.BytesIO] = None, + type: Optional[str] = None, + extension: Optional[str] = None, + word_count: Optional[int] = None, + char_count: Optional[int] = None, + size_in_kb: Optional[float] = None, + duration_in_seconds: Optional[float] = None, + page_count: Optional[int] = None, + slide_count: Optional[int] = None, + entities: Optional[list] = None, + run_id: Optional[str] = None, + status: Optional[str] = None, errors: Optional[list] = None, ): self.file_base64 = file_base64 diff --git a/tests/client/test_skyflow.py b/tests/client/test_skyflow.py index cf9d541..1122448 100644 --- a/tests/client/test_skyflow.py +++ b/tests/client/test_skyflow.py @@ -254,7 +254,7 @@ def test_skyflow_client_add_remove_vault_config(self, mock_validate_vault_config new_config["vault_id"] = "VAULT_ID" skyflow_client.add_vault_config(new_config) - assert mock_validate_vault_config.call_count == 2 + self.assertEqual(mock_validate_vault_config.call_count, 2) self.assertEqual("VAULT_ID", skyflow_client.get_vault_config(new_config["vault_id"]).get("vault_id")) @@ -286,7 +286,7 @@ def test_skyflow_client_add_remove_connection_config(self, mock_validate_connect new_config["connection_id"] = "CONNECTION_ID" skyflow_client.add_connection_config(new_config) - assert mock_validate_connection_config.call_count == 2 + self.assertEqual(mock_validate_connection_config.call_count, 2) self.assertEqual( "CONNECTION_ID", skyflow_client.get_connection_config(new_config["connection_id"]).get("connection_id") ) @@ -430,9 +430,9 @@ def test_update_log_level_emits_deprecation_warning(self): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") client.update_log_level(LogLevel.INFO) - self.assertEqual(len(caught), 1) - self.assertTrue(issubclass(caught[0].category, DeprecationWarning)) - self.assertIn("set_log_level", str(caught[0].message)) + deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)] + self.assertGreaterEqual(len(deprecation_warnings), 1) + self.assertTrue(any("set_log_level" in str(w.message) for w in deprecation_warnings)) def test_update_log_level_warning_points_at_caller(self): client = self._build_client() diff --git a/tests/utils/test__utils.py b/tests/utils/test__utils.py index 9598305..1363ad7 100644 --- a/tests/utils/test__utils.py +++ b/tests/utils/test__utils.py @@ -387,7 +387,7 @@ def test_parse_get_response_successful(self): expected_data = [{"field1": "value1", "field2": "value2"}, {"field1": "value3", "field2": "value4"}] self.assertEqual(result.data, expected_data) - # self.assertEqual(result.errors, None) + self.assertIsNone(result.errors) def test_parse_detokenize_response_with_mixed_records(self): mock_api_response = Mock() From 0b23082b3d242ac7cc4b28b1105f547c0d650bd5 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 May 2026 16:42:13 +0530 Subject: [PATCH 5/6] SK-2833: update skyflow error class when details are none --- skyflow/error/_skyflow_error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyflow/error/_skyflow_error.py b/skyflow/error/_skyflow_error.py index 0de8640..bf47217 100644 --- a/skyflow/error/_skyflow_error.py +++ b/skyflow/error/_skyflow_error.py @@ -12,6 +12,6 @@ def __init__(self, self.http_code = http_code self.grpc_code = grpc_code self.http_status = http_status if http_status else SkyflowMessages.HttpStatus.BAD_REQUEST.value - self.details = details if details is not None else [] + self.details = details if details else None self.request_id = request_id super().__init__(message) \ No newline at end of file From 1da482203b8635b01a3e85316499d5d88241f010 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Wed, 20 May 2026 12:56:11 +0530 Subject: [PATCH 6/6] SK-2833: fix skyflow error messages --- skyflow/utils/_skyflow_messages.py | 7 ++++--- skyflow/utils/validations/_validations.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/skyflow/utils/_skyflow_messages.py b/skyflow/utils/_skyflow_messages.py index 1acfcde..01e1557 100644 --- a/skyflow/utils/_skyflow_messages.py +++ b/skyflow/utils/_skyflow_messages.py @@ -97,7 +97,8 @@ class Error(Enum): INVALID_CONTINUE_ON_ERROR_TYPE = f"{error_prefix} Validation error. Invalid type of continue on error. Specify continue on error as a boolean." TOKENS_PASSED_FOR_TOKEN_MODE_DISABLE = f"{error_prefix} Validation error. 'token_mode' wasn't specified. Set 'token_mode' to 'ENABLE' to insert tokens." INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT = f"{error_prefix} Validation error. 'token_mode' is set to 'ENABLE_STRICT', but some fields are missing tokens. Specify tokens for all fields." - NO_TOKENS_IN_INSERT = f"{error_prefix} Validation error. Tokens weren't specified for records while 'token_strict' was {{}}. Specify tokens." + MISMATCH_OF_FIELDS_AND_TOKENS = f"{error_prefix} Validation error. Keys for values and tokens are not matching. Ensure each values entry and its corresponding tokens entry have the same keys." + NO_TOKENS_IN_INSERT = f"{error_prefix} Validation error. Tokens weren't specified for records while 'token_mode' was {{}}. Specify tokens." BATCH_INSERT_FAILURE = f"{error_prefix} Insert operation failed." GET_FAILURE = f"{error_prefix} Get operation failed." HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT = f"{error_prefix} Validation error. Homogenous is not supported when upsert is passed." @@ -365,8 +366,8 @@ class ErrorLogs(Enum): EMPTY_OR_NULL_ID_IN_IDS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Id can not be null or empty in ids at index {{}}." TOKENIZATION_NOT_SUPPORTED_WITH_REDACTION= f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokenization is not supported when redaction is applied." TOKENIZATION_SUPPORTED_ONLY_WITH_IDS=f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokenization is not supported when column name and values are passed." - TOKENS_NOT_ALLOWED_WITH_BYOT_DISABLE = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens are not allowed when token_strict is DISABLE." - INSUFFICIENT_TOKENS_PASSED_FOR_BYOT_ENABLE_STRICT =f"{ERROR}: [{error_prefix}] Invalid {{}} request. For tokenStrict as ENABLE_STRICT, tokens should be passed for all fields." + TOKENS_NOT_ALLOWED_WITH_BYOT_DISABLE = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens are not allowed when token_mode is DISABLE." + INSUFFICIENT_TOKENS_PASSED_FOR_BYOT_ENABLE_STRICT =f"{ERROR}: [{error_prefix}] Invalid {{}} request. For token_mode as ENABLE_STRICT, tokens should be passed for all fields." TOKENS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens are required." EMPTY_FIELDS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Fields can not be empty." EMPTY_OFFSET = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Offset ca not be empty." diff --git a/skyflow/utils/validations/_validations.py b/skyflow/utils/validations/_validations.py index 59e24d1..6cc2c81 100644 --- a/skyflow/utils/validations/_validations.py +++ b/skyflow/utils/validations/_validations.py @@ -517,14 +517,14 @@ def validate_insert_request(logger, request): raise SkyflowError(SkyflowMessages.Error.TOKENS_PASSED_FOR_TOKEN_MODE_DISABLE.value, invalid_input_error_code) if request.token_mode == TokenMode.ENABLE_STRICT: - if len(request.values) != len(request.tokens): + if not request.tokens or len(request.values) != len(request.tokens): log_error_log(SkyflowMessages.ErrorLogs.INSUFFICIENT_TOKENS_PASSED_FOR_BYOT_ENABLE_STRICT.value.format(RequestOperation.INSERT), logger = logger) raise SkyflowError(SkyflowMessages.Error.INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT.value, invalid_input_error_code) for v, t in zip(request.values, request.tokens): if set(v.keys()) != set(t.keys()): log_error_log(SkyflowMessages.ErrorLogs.MISMATCH_OF_FIELDS_AND_TOKENS.value.format(RequestOperation.INSERT), logger=logger) - raise SkyflowError(SkyflowMessages.Error.INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT.value, invalid_input_error_code) + raise SkyflowError(SkyflowMessages.Error.MISMATCH_OF_FIELDS_AND_TOKENS.value, invalid_input_error_code) def validate_delete_request(logger, request): if not isinstance(request.table, str):