Skip to content

Commit 37ebed7

Browse files
Merge pull request #103 from skyflowapi/release/23.8.2
SK-976/Release/23.8.2
2 parents 4a69139 + cd70527 commit 37ebed7

File tree

6 files changed

+272
-3
lines changed

6 files changed

+272
-3
lines changed

skyflow/_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ class InfoMessages(Enum):
6060
INSERT_DATA_SUCCESS = "Data has been inserted successfully."
6161
DETOKENIZE_SUCCESS = "Data has been detokenized successfully."
6262
GET_BY_ID_SUCCESS = "Data fetched from ID successfully."
63+
QUERY_SUCCESS = "Query executed successfully."
6364
BEARER_TOKEN_RECEIVED = "tokenProvider returned token successfully."
6465
INSERT_TRIGGERED = "Insert method triggered."
6566
DETOKENIZE_TRIGGERED = "Detokenize method triggered."
6667
GET_BY_ID_TRIGGERED = "Get by ID triggered."
6768
INVOKE_CONNECTION_TRIGGERED = "Invoke connection triggered."
69+
QUERY_TRIGGERED = "Query method triggered."
6870
GENERATE_BEARER_TOKEN_TRIGGERED = "Generate bearer token triggered"
6971
GENERATE_BEARER_TOKEN_SUCCESS = "Generate bearer token returned successfully"
7072
IS_TOKEN_VALID_TRIGGERED = "isTokenValid() triggered"
@@ -87,6 +89,7 @@ class InterfaceName(Enum):
8789
GET = "client.get"
8890
UPDATE = "client.update"
8991
INVOKE_CONNECTION = "client.invoke_connection"
92+
QUERY = "client.query"
9093
GENERATE_BEARER_TOKEN = "service_account.generate_bearer_token"
9194

9295
IS_TOKEN_VALID = "service_account.isTokenValid"

skyflow/errors/_skyflow_errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class SkyflowErrorMessages(Enum):
8585
INVALID_UPSERT_COLUMN_TYPE = "upsert object column key has value of type %s, expected string"
8686
EMPTY_UPSERT_OPTION_TABLE = "upsert object table value is empty string at index %s, expected non-empty string"
8787
EMPTY_UPSERT_OPTION_COLUMN = "upsert object column value is empty string at index %s, expected non-empty string"
88+
QUERY_KEY_ERROR = "Query key is missing from payload"
89+
INVALID_QUERY_TYPE = "Query key has value of type %s, expected string"
90+
EMPTY_QUERY = "Query key cannot be empty"
91+
INVALID_QUERY_COMMAND = "only SELECT commands are supported, %s command was passed instead"
8892
SERVER_ERROR = "Server returned errors, check SkyflowError.data for more"
8993

9094
BATCH_INSERT_PARTIAL_SUCCESS = "Insert Operation is partially successful"

skyflow/vault/_client.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
from ._delete import deleteProcessResponse
99
from ._insert import getInsertRequestBody, processResponse, convertResponse
1010
from ._update import sendUpdateRequests, createUpdateResponseBody
11-
from ._config import Configuration, DeleteOptions
12-
from ._config import DetokenizeOptions, InsertOptions, ConnectionConfig, UpdateOptions
11+
from ._config import Configuration, DeleteOptions, DetokenizeOptions, InsertOptions, ConnectionConfig, UpdateOptions, QueryOptions
1312
from ._connection import createRequest
1413
from ._detokenize import sendDetokenizeRequests, createDetokenizeResponseBody
1514
from ._get_by_id import sendGetByIdRequests, createGetResponseBody
@@ -18,7 +17,7 @@
1817
from skyflow.errors._skyflow_errors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
1918
from skyflow._utils import log_info, log_error, InfoMessages, InterfaceName, getMetrics
2019
from ._token import tokenProviderWrapper
21-
20+
from ._query import getQueryRequestBody, getQueryResponse
2221

2322
class Client:
2423
def __init__(self, config: Configuration):
@@ -147,6 +146,28 @@ def invoke_connection(self, config: ConnectionConfig):
147146
session.close()
148147
return processResponse(response, interface=interface)
149148

149+
def query(self, queryInput, options: QueryOptions = QueryOptions()):
150+
interface = InterfaceName.QUERY.value
151+
log_info(InfoMessages.QUERY_TRIGGERED.value, interface=interface)
152+
153+
self._checkConfig(interface)
154+
155+
jsonBody = getQueryRequestBody(queryInput, options)
156+
requestURL = self._get_complete_vault_url() + "/query"
157+
self.storedToken = tokenProviderWrapper(
158+
self.storedToken, self.tokenProvider, interface)
159+
headers = {
160+
"Content-Type": "application/json",
161+
"Authorization": "Bearer " + self.storedToken,
162+
"sky-metadata": json.dumps(getMetrics())
163+
}
164+
165+
response = requests.post(requestURL, data=jsonBody, headers=headers)
166+
result = getQueryResponse(response)
167+
168+
log_info(InfoMessages.QUERY_SUCCESS.value, interface)
169+
return result
170+
150171
def _checkConfig(self, interface):
151172
'''
152173
Performs basic check on the given client config

skyflow/vault/_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def __init__(self, tokens: bool=True):
4242
class DeleteOptions:
4343
def __init__(self, tokens: bool=False):
4444
self.tokens = tokens
45+
46+
class QueryOptions:
47+
def __init__(self):
48+
pass
4549

4650
class DetokenizeOptions:
4751
def __init__(self, continueOnError: bool=True):

skyflow/vault/_query.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'''
2+
Copyright (c) 2022 Skyflow, Inc.
3+
'''
4+
import json
5+
6+
import requests
7+
from ._config import QueryOptions
8+
from requests.models import HTTPError
9+
from skyflow.errors._skyflow_errors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
10+
from skyflow._utils import InterfaceName
11+
12+
interface = InterfaceName.QUERY.value
13+
14+
15+
def getQueryRequestBody(data, options):
16+
try:
17+
query = data["query"]
18+
except KeyError:
19+
raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT,
20+
SkyflowErrorMessages.QUERY_KEY_ERROR, interface=interface)
21+
22+
if not isinstance(query, str):
23+
queryType = str(type(query))
24+
raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_QUERY_TYPE.value % queryType, interface=interface)
25+
26+
if not query.strip():
27+
raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT,SkyflowErrorMessages.EMPTY_QUERY.value, interface=interface)
28+
29+
requestBody = {"query": query}
30+
try:
31+
jsonBody = json.dumps(requestBody)
32+
except Exception as e:
33+
raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_JSON.value % (
34+
'query payload'), interface=interface)
35+
36+
return jsonBody
37+
38+
def getQueryResponse(response: requests.Response, interface=interface):
39+
statusCode = response.status_code
40+
content = response.content.decode('utf-8')
41+
try:
42+
response.raise_for_status()
43+
try:
44+
return json.loads(content)
45+
except:
46+
raise SkyflowError(
47+
statusCode, SkyflowErrorMessages.RESPONSE_NOT_JSON.value % content, interface=interface)
48+
except HTTPError:
49+
message = SkyflowErrorMessages.API_ERROR.value % statusCode
50+
if response != None and response.content != None:
51+
try:
52+
errorResponse = json.loads(content)
53+
if 'error' in errorResponse and type(errorResponse['error']) == type({}) and 'message' in errorResponse['error']:
54+
message = errorResponse['error']['message']
55+
except:
56+
message = SkyflowErrorMessages.RESPONSE_NOT_JSON.value % content
57+
raise SkyflowError(SkyflowErrorCodes.INVALID_INDEX, message, interface=interface)
58+
error = {"error": {}}
59+
if 'x-request-id' in response.headers:
60+
message += ' - request id: ' + response.headers['x-request-id']
61+
error['error'].update({"code": statusCode, "description": message})
62+
raise SkyflowError(SkyflowErrorCodes.SERVER_ERROR, SkyflowErrorMessages.SERVER_ERROR.value, error, interface=interface)

tests/vault/test_query.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'''
2+
Copyright (c) 2022 Skyflow, Inc.
3+
'''
4+
import json
5+
import unittest
6+
import os
7+
from unittest import mock
8+
import requests
9+
from requests.models import Response
10+
from skyflow.vault._query import getQueryRequestBody, getQueryResponse
11+
from skyflow.errors._skyflow_errors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
12+
from skyflow.vault._client import Client
13+
from skyflow.vault._config import Configuration, QueryOptions
14+
15+
class TestQuery(unittest.TestCase):
16+
17+
def setUp(self) -> None:
18+
self.dataPath = os.path.join(os.getcwd(), 'tests/vault/data/')
19+
query = "SELECT * FROM pii_fields WHERE skyflow_id='3ea3861-x107-40w8-la98-106sp08ea83f'"
20+
self.data = {"query": query}
21+
self.mockRequest = {"records": [query]}
22+
23+
self.mockResponse = {
24+
"records": [
25+
{
26+
"fields": {
27+
"card_number": "XXXXXXXXXXXX1111",
28+
"card_pin": "*REDACTED*",
29+
"cvv": "",
30+
"expiration_date": "*REDACTED*",
31+
"expiration_month": "*REDACTED*",
32+
"expiration_year": "*REDACTED*",
33+
"name": "a***te",
34+
"skyflow_id": "3ea3861-x107-40w8-la98-106sp08ea83f",
35+
"ssn": "XXX-XX-6789",
36+
"zip_code": None
37+
},
38+
"tokens": None
39+
}
40+
]
41+
}
42+
43+
self.requestId = '5d5d7e21-c789-9fcc-ba31-2a279d3a28ef'
44+
45+
self.mockApiError = {
46+
"error": {
47+
"grpc_code": 13,
48+
"http_code": 500,
49+
"message": "ERROR (internal_error): Could not find Notebook Mapping Notebook Name was not found",
50+
"http_status": "Internal Server Error",
51+
"details": []
52+
}
53+
}
54+
55+
self.mockFailResponse = {
56+
"error": {
57+
"code": 500,
58+
"description": "ERROR (internal_error): Could not find Notebook Mapping Notebook Name was not found - request id: 5d5d7e21-c789-9fcc-ba31-2a279d3a28ef"
59+
}
60+
}
61+
62+
self.queryOptions = QueryOptions()
63+
64+
return super().setUp()
65+
66+
def getDataPath(self, file):
67+
return self.dataPath + file + '.json'
68+
69+
def testGetQueryRequestBodyWithValidBody(self):
70+
body = json.loads(getQueryRequestBody(self.data, self.queryOptions))
71+
expectedOutput = {
72+
"query": "SELECT * FROM pii_fields WHERE skyflow_id='3ea3861-x107-40w8-la98-106sp08ea83f'",
73+
}
74+
self.assertEqual(body, expectedOutput)
75+
76+
def testGetQueryRequestBodyNoQuery(self):
77+
invalidData = {"invalidKey": self.data["query"]}
78+
try:
79+
getQueryRequestBody(invalidData, self.queryOptions)
80+
self.fail('Should have thrown an error')
81+
except SkyflowError as e:
82+
self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
83+
self.assertEqual(
84+
e.message, SkyflowErrorMessages.QUERY_KEY_ERROR.value)
85+
86+
def testGetQueryRequestBodyInvalidType(self):
87+
invalidData = {"query": ['SELECT * FROM table_name']}
88+
try:
89+
getQueryRequestBody(invalidData, self.queryOptions)
90+
self.fail('Should have thrown an error')
91+
except SkyflowError as e:
92+
self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
93+
self.assertEqual(
94+
e.message, SkyflowErrorMessages.INVALID_QUERY_TYPE.value % (str(type(invalidData["query"]))))
95+
96+
def testGetQueryRequestBodyEmptyBody(self):
97+
invalidData = {"query": ''}
98+
try:
99+
getQueryRequestBody(invalidData, self.queryOptions)
100+
self.fail('Should have thrown an error')
101+
except SkyflowError as e:
102+
self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
103+
self.assertEqual(
104+
e.message, SkyflowErrorMessages.EMPTY_QUERY.value)
105+
106+
def testGetQueryValidResponse(self):
107+
response = Response()
108+
response.status_code = 200
109+
response._content = b'{"key": "value"}'
110+
try:
111+
responseDict = getQueryResponse(response)
112+
self.assertDictEqual(responseDict, {'key': 'value'})
113+
except SkyflowError as e:
114+
self.fail()
115+
116+
def testClientInit(self):
117+
config = Configuration(
118+
'vaultid', 'https://skyflow.com', lambda: 'test')
119+
client = Client(config)
120+
self.assertEqual(client.vaultURL, 'https://skyflow.com')
121+
self.assertEqual(client.vaultID, 'vaultid')
122+
self.assertEqual(client.tokenProvider(), 'test')
123+
124+
def testGetQueryResponseSuccessInvalidJson(self):
125+
invalid_response = Response()
126+
invalid_response.status_code = 200
127+
invalid_response._content = b'invalid-json'
128+
try:
129+
getQueryResponse(invalid_response)
130+
self.fail('not failing on invalid json')
131+
except SkyflowError as se:
132+
self.assertEqual(se.code, 200)
133+
self.assertEqual(
134+
se.message, SkyflowErrorMessages.RESPONSE_NOT_JSON.value % 'invalid-json')
135+
136+
def testGetQueryResponseFailInvalidJson(self):
137+
invalid_response = mock.Mock(
138+
spec=requests.Response,
139+
status_code=404,
140+
content=b'error'
141+
)
142+
invalid_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Not found")
143+
try:
144+
getQueryResponse(invalid_response)
145+
self.fail('Not failing on invalid error json')
146+
except SkyflowError as se:
147+
self.assertEqual(se.code, 404)
148+
self.assertEqual(
149+
se.message, SkyflowErrorMessages.RESPONSE_NOT_JSON.value % 'error')
150+
151+
def testGetQueryResponseFail(self):
152+
response = mock.Mock(
153+
spec=requests.Response,
154+
status_code=500,
155+
content=json.dumps(self.mockApiError).encode('utf-8')
156+
)
157+
response.headers = {"x-request-id": self.requestId}
158+
response.raise_for_status.side_effect = requests.exceptions.HTTPError("Server Error")
159+
try:
160+
getQueryResponse(response)
161+
self.fail('not throwing exception when error code is 500')
162+
except SkyflowError as e:
163+
self.assertEqual(e.code, 500)
164+
self.assertEqual(e.message, SkyflowErrorMessages.SERVER_ERROR.value)
165+
self.assertDictEqual(e.data, self.mockFailResponse)
166+
167+
def testQueryInvalidToken(self):
168+
config = Configuration('id', 'url', lambda: 'invalid-token')
169+
try:
170+
Client(config).query({'query': 'SELECT * FROM table_name'})
171+
self.fail()
172+
except SkyflowError as e:
173+
self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
174+
self.assertEqual(
175+
e.message, SkyflowErrorMessages.TOKEN_PROVIDER_INVALID_TOKEN.value)

0 commit comments

Comments
 (0)