Skip to content

Commit f994e2b

Browse files
authored
Merge pull request #58 from keboola/PS-1037-abs
PS-1037 Add support for azure stacks
2 parents 65c5cc6 + c91a508 commit f994e2b

6 files changed

Lines changed: 149 additions & 60 deletions

File tree

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 120

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ language: bash
33
services:
44
- docker
55
before_script:
6+
- docker login --username "$DOCKERHUB_USER" --password "$DOCKERHUB_TOKEN"
67
- docker-compose build sapi-python-client
78
script:
89
- docker-compose run --rm --entrypoint=flake8 sapi-python-client
910
- docker-compose run --rm -e KBC_TEST_TOKEN -e KBC_TEST_API_URL sapi-python-client -m unittest discover
11+
- docker-compose run --rm -e KBC_TEST_TOKEN=$KBC_AZ_TEST_TOKEN -e KBC_TEST_API_URL=$KBC_AZ_TEST_API_URL sapi-python-client -m unittest discover
1012
after_success:
1113
- docker images
1214
deploy:

kbcstorage/files.py

Lines changed: 120 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
.. _here:
77
http://docs.keboola.apiary.io/#reference/files/
88
"""
9+
import json
910
import os
1011
import boto3
1112
import requests
1213

14+
from azure.storage.blob import BlobServiceClient, ContentSettings
1315
from kbcstorage.base import Endpoint
1416

1517

@@ -71,36 +73,20 @@ def upload_file(self, file_path, tags=None, is_public=False,
7173
if compress:
7274
import gzip
7375
import shutil
74-
with open(file_path, 'rb') as f_in, gzip.open(file_path + '.gz',
75-
'wb') as f_out:
76+
with open(file_path, 'rb') as f_in, \
77+
gzip.open(file_path + '.gz', 'wb') as f_out:
7678
shutil.copyfileobj(f_in, f_out)
7779
file_path = file_path + '.gz'
7880
file_name = os.path.basename(file_path)
7981
size = os.path.getsize(file_path)
8082
file_resource = self.prepare_upload(file_name, size, tags, is_public,
8183
is_permanent, is_encrypted,
8284
is_sliced, do_notify, True)
83-
upload_params = file_resource['uploadParams']
84-
key_id = upload_params['credentials']['AccessKeyId']
85-
key = upload_params['credentials']['SecretAccessKey']
86-
token = upload_params['credentials']['SessionToken']
87-
s3 = boto3.resource('s3', aws_access_key_id=key_id,
88-
aws_secret_access_key=key,
89-
aws_session_token=token,
90-
region_name=file_resource['region'])
85+
if file_resource['provider'] == 'azure':
86+
self.__upload_to_azure(file_resource, file_path)
87+
elif file_resource['provider'] == 'aws':
88+
self.__upload_to_aws(file_resource, file_path, is_encrypted)
9189

92-
s3_object = s3.Object(bucket_name=upload_params['bucket'],
93-
key=upload_params['key'])
94-
disposition = 'attachment; filename={};'.format(file_resource['name'])
95-
with open(file_path, mode='rb') as file:
96-
if is_encrypted:
97-
encryption = upload_params['x-amz-server-side-encryption']
98-
s3_object.put(ACL=upload_params['acl'], Body=file,
99-
ContentDisposition=disposition,
100-
ServerSideEncryption=encryption)
101-
else:
102-
s3_object.put(ACL=upload_params['acl'], Body=file,
103-
ContentDisposition=disposition)
10490
return file_resource['id']
10591

10692
def prepare_upload(self, name, size_bytes=None, tags=None, is_public=False,
@@ -193,32 +179,116 @@ def download(self, file_id, local_path):
193179
os.mkdir(local_path)
194180
file_info = self.detail(file_id=file_id, federation_token=True)
195181
local_file = os.path.join(local_path, file_info['name'])
196-
s3 = boto3.resource(
197-
's3',
198-
aws_access_key_id=file_info['credentials']['AccessKeyId'],
199-
aws_secret_access_key=file_info['credentials']['SecretAccessKey'],
200-
aws_session_token=file_info['credentials']['SessionToken'],
201-
region_name=file_info['region']
202-
)
203-
if file_info['isSliced']:
204-
manifest = requests.get(url=file_info['url']).json()
205-
file_names = []
206-
for entry in manifest["entries"]:
207-
full_path = entry["url"]
208-
file_name = full_path.rsplit("/", 1)[1]
209-
file_names.append(file_name)
210-
splitted_path = full_path.split("/")
211-
file_key = "/".join(splitted_path[3:])
212-
bucket = s3.Bucket(file_info['s3Path']['bucket'])
213-
bucket.download_file(file_key, file_name)
214-
# merge the downloaded files
215-
with open(local_file, mode='wb') as out_file:
216-
for file_name in file_names:
217-
with open(file_name, mode='rb') as in_file:
218-
for line in in_file:
219-
out_file.write(line)
220-
os.remove(file_name)
221-
else:
222-
bucket = s3.Bucket(file_info["s3Path"]["bucket"])
223-
bucket.download_file(file_info["s3Path"]["key"], local_file)
182+
if file_info['provider'] == 'azure':
183+
if file_info['isSliced']:
184+
self.__download_sliced_file_from_azure(file_info, local_file)
185+
else:
186+
self.__download_file_from_azure(file_info, local_file)
187+
elif file_info['provider'] == 'aws':
188+
s3 = boto3.resource(
189+
's3',
190+
aws_access_key_id=file_info['credentials']['AccessKeyId'],
191+
aws_secret_access_key=file_info['credentials']['SecretAccessKey'],
192+
aws_session_token=file_info['credentials']['SessionToken'],
193+
region_name=file_info['region']
194+
)
195+
if file_info['isSliced']:
196+
self.__download_sliced_file_from_aws(file_info, local_file, s3)
197+
else:
198+
self.__download_file_from_aws(file_info, local_file, s3)
224199
return local_file
200+
201+
def __upload_to_azure(self, preparation_result, file_path):
202+
blob_client = self.__get_blob_client(
203+
preparation_result['absUploadParams']['absCredentials']['SASConnectionString'],
204+
preparation_result['absUploadParams']['container'],
205+
preparation_result['absUploadParams']['blobName']
206+
)
207+
with open(file_path, "rb") as blob_data:
208+
blob_client.upload_blob(
209+
blob_data,
210+
blob_type='BlockBlob',
211+
content_settings=ContentSettings(
212+
content_disposition='attachment;filename="%s"' % (preparation_result['name'])
213+
)
214+
)
215+
216+
def __upload_to_aws(self, prepare_result, file_path, is_encrypted):
217+
upload_params = prepare_result['uploadParams']
218+
key_id = upload_params['credentials']['AccessKeyId']
219+
key = upload_params['credentials']['SecretAccessKey']
220+
token = upload_params['credentials']['SessionToken']
221+
s3 = boto3.resource('s3', aws_access_key_id=key_id,
222+
aws_secret_access_key=key,
223+
aws_session_token=token,
224+
region_name=prepare_result['region'])
225+
s3_object = s3.Object(bucket_name=upload_params['bucket'], key=upload_params['key'])
226+
disposition = 'attachment; filename={};'.format(prepare_result['name'])
227+
with open(file_path, mode='rb') as file:
228+
if is_encrypted:
229+
encryption = upload_params['x-amz-server-side-encryption']
230+
s3_object.put(ACL=upload_params['acl'], Body=file,
231+
ContentDisposition=disposition, ServerSideEncryption=encryption)
232+
else:
233+
s3_object.put(ACL=upload_params['acl'], Body=file,
234+
ContentDisposition=disposition)
235+
236+
def __download_file_from_aws(self, file_info, destination, s3):
237+
bucket = s3.Bucket(file_info["s3Path"]["bucket"])
238+
bucket.download_file(file_info["s3Path"]["key"], destination)
239+
240+
def __download_sliced_file_from_aws(self, file_info, destination, s3):
241+
manifest = requests.get(url=file_info['url']).json()
242+
file_names = []
243+
for entry in manifest["entries"]:
244+
full_path = entry["url"]
245+
file_name = full_path.rsplit("/", 1)[1]
246+
file_names.append(file_name)
247+
splitted_path = full_path.split("/")
248+
file_key = "/".join(splitted_path[3:])
249+
bucket = s3.Bucket(file_info['s3Path']['bucket'])
250+
bucket.download_file(file_key, file_name)
251+
self.__merge_split_files(file_names, destination)
252+
253+
def __download_file_from_azure(self, file_info, destination):
254+
blob_client = self.__get_blob_client(
255+
file_info['absCredentials']['SASConnectionString'],
256+
file_info['absPath']['container'],
257+
file_info['absPath']['name']
258+
)
259+
with open(destination, "wb") as downloaded_blob:
260+
download_stream = blob_client.download_blob()
261+
downloaded_blob.write(download_stream.readall())
262+
263+
def __download_sliced_file_from_azure(self, file_info, destination):
264+
blob_service_client = BlobServiceClient.from_connection_string(
265+
file_info['absCredentials']['SASConnectionString']
266+
)
267+
container_client = blob_service_client.get_container_client(
268+
container=file_info['absPath']['container']
269+
)
270+
manifest_stream = container_client.download_blob(
271+
file_info['absPath']['name'] + 'manifest'
272+
)
273+
manifest = json.loads(manifest_stream.readall())
274+
file_names = []
275+
for entry in manifest['entries']:
276+
blob_path = entry['url'].split('blob.core.windows.net/%s/' % (file_info['absPath']['container']))[1]
277+
full_path = entry["url"]
278+
file_name = full_path.rsplit("/", 1)[1]
279+
file_names.append(file_name)
280+
with open(file_name, "wb") as file_slice:
281+
file_slice.write(container_client.download_blob(blob_path).readall())
282+
self.__merge_split_files(file_names, destination)
283+
284+
def __merge_split_files(self, file_names, destination):
285+
with open(destination, mode='wb') as out_file:
286+
for file_name in file_names:
287+
with open(file_name, mode='rb') as in_file:
288+
for line in in_file:
289+
out_file.write(line)
290+
os.remove(file_name)
291+
292+
def __get_blob_client(self, connection_string, container, blob_name):
293+
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
294+
return blob_service_client.get_blob_client(container=container, blob=blob_name)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
packages=find_packages(exclude=['tests']),
1313
install_requires=[
1414
'boto3',
15+
'azure-storage-blob',
1516
'requests'
1617
],
1718
test_suite='tests',

tests/functional/test_files.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,29 @@ def test_download_file_credentials(self):
102102
with self.subTest():
103103
self.assertEqual(file_id, file_info['id'])
104104
with self.subTest():
105-
self.assertTrue('credentials' in file_info)
106-
with self.subTest():
107-
self.assertTrue('AccessKeyId' in file_info['credentials'])
108-
with self.subTest():
109-
self.assertTrue('SecretAccessKey' in file_info['credentials'])
110-
with self.subTest():
111-
self.assertTrue('SessionToken' in file_info['credentials'])
105+
self.assertTrue(file_info['provider'] in ['aws', 'azure'])
106+
if file_info['provider'] == 'aws':
107+
with self.subTest():
108+
self.assertTrue('credentials' in file_info)
109+
with self.subTest():
110+
self.assertTrue('AccessKeyId' in file_info['credentials'])
111+
with self.subTest():
112+
self.assertTrue('SecretAccessKey' in file_info['credentials'])
113+
with self.subTest():
114+
self.assertTrue('SessionToken' in file_info['credentials'])
115+
elif file_info['provider'] == 'azure':
116+
with self.subTest():
117+
self.assertTrue('absCredentials' in file_info)
118+
with self.subTest():
119+
self.assertTrue('SASConnectionString' in file_info['absCredentials'])
120+
with self.subTest():
121+
self.assertTrue('expiration' in file_info['absCredentials'])
122+
with self.subTest():
123+
self.assertTrue('absPath' in file_info)
124+
with self.subTest():
125+
self.assertTrue('container' in file_info['absPath'])
126+
with self.subTest():
127+
self.assertTrue('name' in file_info['absPath'])
112128

113129
def test_download_file(self):
114130
file, path = tempfile.mkstemp(prefix='sapi-test')

tests/functional/test_tables.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ def test_table_detail(self):
8484
with self.subTest():
8585
self.assertEqual('some-table', table_info['name'])
8686
with self.subTest():
87-
self.assertEqual('https://connection.keboola.com/v2/storage/'
88-
'tables/in.c-py-test-tables.some-table',
89-
table_info['uri'])
87+
self.assertTrue('in.c-py-test-tables.some-table' in table_info['uri'])
9088
with self.subTest():
9189
self.assertEqual([], table_info['primaryKey'])
9290
with self.subTest():

0 commit comments

Comments
 (0)