From cade7cbe660f91c251a8355334ed93efc439c4ef Mon Sep 17 00:00:00 2001 From: Matt <394065+fieldse@users.noreply.github.com> Date: Tue, 23 Oct 2018 18:23:21 +0800 Subject: [PATCH 01/19] Move delete -> delete_version, add delete -> "hide" endpoint in docs --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 950cde7..0fc5db5 100644 --- a/README.md +++ b/README.md @@ -162,13 +162,22 @@ save_file.write(downloaded_file.read()) save_file.close() ``` -#### Delete a file +#### Delete a file version + +```python +file.delete_version() +``` + +This deletes a single version of a file. (See the [docs on File Versions](https://www.backblaze.com/b2/docs/b2_delete_file_version.html) at Backblaze for explanation) + +#### Hide (aka "Soft-delete") a file ```python file.delete() ``` -NOTE: There is no confirmation and this will delete all of a file's versions. +This hides a file (aka "soft-delete") so that downloading by name will not find the file, but previous versions of the file are still stored. (See the [docs on Hiding file](https://www.backblaze.com/b2/docs/b2_hide_file.html) at Backblaze for details) + ## LICENSE From 659551da24a8b2d694da952cf0d1f34871a21710 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Tue, 23 Oct 2018 19:38:20 +0800 Subject: [PATCH 02/19] Add API endpoints file. Simplify download URLs. b2_file: Add delete / delete_version stubs --- b2blaze/api.py | 34 ++++++++++++++++++++++++++++++++++ b2blaze/connector.py | 14 +++++++------- b2blaze/models/b2_file.py | 24 +++++++++++++++++++++--- 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 b2blaze/api.py diff --git a/b2blaze/api.py b/b2blaze/api.py new file mode 100644 index 0000000..441ff88 --- /dev/null +++ b/b2blaze/api.py @@ -0,0 +1,34 @@ +# api.py +# BackBlaze API endpoints + +API_VERSION = '/b2api/v2' +BASE_API_URL = 'https://api.backblazeb2.com' + API_VERSION + + +class AuthAPI(): + authorize = '/b2_authorize_account' + +class FileAPI(): + delete = '/b2_hide_file' + delete_version = '/b2_delete_file_version' + file_info = '/b2_get_file_info' + download_by_id = '/b2_download_file_by_id' + + +class BucketAPI(): + create = '/b2_create_bucket' + delete = '/b2_delete_bucket' + list_buckets = '/b2_list_buckets' + list_files = '/b2_list_file_names' + upload = '/b2_get_upload_url' + upload_large = '/b2_start_large_file' + upload_large_part = '/b2_get_upload_part_url' + upload_large_finish = '/b2_finish_large_file' + + + +class AccountAPI(): + pass + +class ApplicationKeyAPI(): + pass \ No newline at end of file diff --git a/b2blaze/connector.py b/b2blaze/connector.py index 2d9efd4..c2663e7 100644 --- a/b2blaze/connector.py +++ b/b2blaze/connector.py @@ -8,13 +8,12 @@ import sys from hashlib import sha1 from b2blaze.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress +from api import BASE_API_URL, API_VERSION, AuthAPI, FileAPI class B2Connector(object): """ """ - auth_url = 'https://api.backblazeb2.com/b2api/v1' - def __init__(self, key_id, application_key): """ @@ -53,15 +52,16 @@ def _authorize(self): :return: """ - path = self.auth_url + '/b2_authorize_account' + path = BASE_API_URL + AuthAPI.authorize + result = requests.get(path, auth=HTTPBasicAuth(self.key_id, self.application_key)) if result.status_code == 200: result_json = result.json() self.authorized_at = datetime.datetime.utcnow() self.account_id = result_json['accountId'] self.auth_token = result_json['authorizationToken'] - self.api_url = result_json['apiUrl'] + '/b2api/v1' - self.download_url = result_json['downloadUrl'] + '/file/' + self.api_url = result_json['apiUrl'] + API_VERSION + self.download_url = result_json['downloadUrl'] + API_VERSION + FileAPI.download_by_id self.recommended_part_size = result_json['recommendedPartSize'] self.api_session = requests.Session() self.api_session.headers.update({ @@ -164,7 +164,7 @@ def download_file(self, file_id): :param file_id: :return: """ - download_by_id_url = self.download_url.split('file/')[0] + '/b2api/v1/b2_download_file_by_id' + url = self.download_url params = { 'fileId': file_id } @@ -172,5 +172,5 @@ def download_file(self, file_id): 'Authorization': self.auth_token } - return requests.get(download_by_id_url, headers=headers, params=params) + return requests.get(url, headers=headers, params=params) diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index c52dea3..5de52fa 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -4,6 +4,7 @@ from io import BytesIO from ..utilities import b2_url_encode, b2_url_decode, decode_error from ..b2_exceptions import B2RequestError +from ..api import FileAPI class B2File(object): """ @@ -41,11 +42,29 @@ def __init__(self, connector, parent_list, fileId, fileName, contentSha1, conten self.deleted = False def delete(self): + """ Soft-delete a file (hide it from files list, but previous versions are saved.) + :return: """ + path = FileAPI.delete + params = { + 'fileId': self.file_id, + 'fileName': b2_url_encode(self.file_name) + } + response = self.connector.make_request(path=path, method='post', params=params) + if response.status_code == 200: + self.deleted = True + del self.parent_list._files_by_name[self.file_name] + del self.parent_list._files_by_id[self.file_id] + else: + raise B2RequestError(decode_error(response)) + #TODO: Raise Error + + def delete_version(self): + """ Delete a file version (Does not delete entire file history: only most recent version) :return: """ - path = '/b2_delete_file_version' + path = FileAPI.delete_version params = { 'fileId': self.file_id, 'fileName': b2_url_encode(self.file_name) @@ -76,5 +95,4 @@ def url(self): :return: file download url """ - return self.connector.download_url.split('file/')[0] \ - + 'b2api/v1/b2_download_file_by_id?fileId=' + self.file_id \ No newline at end of file + return self.connector.download_url + '?fileId=' + self.file_id From 0d181ef0af904be172d5b8714adbd9c9cea5d7c2 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Tue, 23 Oct 2018 20:37:24 +0800 Subject: [PATCH 03/19] b2_file: fixed delete method; connector: fixed import of .api --- b2blaze/connector.py | 6 +++--- b2blaze/models/b2_file.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/b2blaze/connector.py b/b2blaze/connector.py index c2663e7..3b71a80 100644 --- a/b2blaze/connector.py +++ b/b2blaze/connector.py @@ -4,11 +4,11 @@ import requests import datetime from requests.auth import HTTPBasicAuth -from b2blaze.b2_exceptions import B2AuthorizationError, B2RequestError, B2InvalidRequestType +from b2blaze_fork.b2_exceptions import B2AuthorizationError, B2RequestError, B2InvalidRequestType import sys from hashlib import sha1 -from b2blaze.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress -from api import BASE_API_URL, API_VERSION, AuthAPI, FileAPI +from b2blaze_fork.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress +from .api import BASE_API_URL, API_VERSION, AuthAPI, FileAPI class B2Connector(object): """ diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index 5de52fa..0d08315 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -47,7 +47,7 @@ def delete(self): """ path = FileAPI.delete params = { - 'fileId': self.file_id, + 'bucketId': self.parent_list.bucket.bucket_id, 'fileName': b2_url_encode(self.file_name) } response = self.connector.make_request(path=path, method='post', params=params) From e0a0f9353863fa97c065825a9b92208d0cd4f467 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 13:17:20 +0800 Subject: [PATCH 04/19] setup.py: Update package documentation --- b2blaze/connector.py | 4 ++-- setup.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/b2blaze/connector.py b/b2blaze/connector.py index 3b71a80..3d9f4be 100644 --- a/b2blaze/connector.py +++ b/b2blaze/connector.py @@ -4,10 +4,10 @@ import requests import datetime from requests.auth import HTTPBasicAuth -from b2blaze_fork.b2_exceptions import B2AuthorizationError, B2RequestError, B2InvalidRequestType +from b2blaze.b2_exceptions import B2AuthorizationError, B2RequestError, B2InvalidRequestType import sys from hashlib import sha1 -from b2blaze_fork.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress +from b2blaze.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress from .api import BASE_API_URL, API_VERSION, AuthAPI, FileAPI class B2Connector(object): diff --git a/setup.py b/setup.py index d572c19..12ea8b2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.1.10' +VERSION = '0.1.11' from os import path this_directory = path.abspath(path.dirname(__file__)) @@ -11,14 +11,14 @@ setup(name='b2blaze', version=VERSION, - description='b2blaze library for connecting to Backblaze B2', + description='Forked from George Sibble\'s B2Blaze (0.1.10). All credits to author. Original package: https://github.com/sibblegp/b2blaze', long_description=long_description, long_description_content_type='text/markdown', packages=find_packages(), author='George Sibble', author_email='gsibble@gmail.com', python_requires='>=2.7', - url='https://github.com/sibblegp/b2blaze', + url='https://github.com/fieldse/b2blaze-fork', install_requires=[ 'requests==2.19.1' ], From 5c5b1408f13d57ecc2be2866ad1d38b4d7a50cb3 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 14:11:07 +0800 Subject: [PATCH 05/19] Cleaned up requirements.txt --- requirements.txt | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9e8b7cd..81f557a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,48 +1,17 @@ -asn1crypto==0.24.0 -astroid==1.6.5 -atomicwrites==1.1.5 -attrs==18.1.0 -backports.functools-lru-cache==1.5 -blessings==1.7 -bpython==0.17.1 -certifi==2018.4.16 -cffi==1.11.5 +atomicwrites==1.2.1 +attrs==18.2.0 +certifi==2018.10.15 chardet==3.0.4 -configparser==3.5.0 coverage==4.5.1 -cryptography==2.3 -curtsies==0.3.0 -enum34==1.1.6 -funcsigs==1.0.2 -futures==3.2.0 -greenlet==0.4.13 idna==2.7 -ipaddress==1.0.22 -isort==4.3.4 -lazy-object-proxy==1.3.1 -mccabe==0.6.1 mock==2.0.0 -more-itertools==4.2.0 -ndg-httpsclient==0.5.0 +more-itertools==4.3.0 nose==1.3.7 -pbr==4.0.4 -pkginfo==1.4.2 +pbr==5.1.0 pluggy==0.6.0 py==1.5.4 -pyasn1==0.4.3 -pycparser==2.18 -Pygments==2.2.0 -pylint==1.9.2 -pyOpenSSL==18.0.0 pytest==3.6.2 requests==2.19.1 -requests-toolbelt==0.8.0 -singledispatch==3.4.0.3 six==1.11.0 sure==1.4.11 -tqdm==4.23.4 -twine==1.11.0 -typing==3.6.4 urllib3==1.23 -wcwidth==0.1.7 -wrapt==1.10.11 From 8f59c4a8a29b5565add25570ebf7cf9bb4aca049 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 15:56:55 +0800 Subject: [PATCH 06/19] Create instal script. file_list.py: update files list after upload --- b2blaze/models/file_list.py | 8 ++------ install.sh | 13 +++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100755 install.sh diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index 952dd62..842223c 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -100,12 +100,6 @@ def get(self, file_name=None, file_id=None): else: raise ValueError('file_name or file_id must be passed') - # self._update_files_list() - # if file_name is not None: - # return self._files_by_name.get(file_name, None) - # else: - # return self._files_by_id.get(file_id, None) - # pass def upload(self, contents, file_name, mime_content_type=None, content_length=None, progress_listener=None): """ @@ -132,6 +126,8 @@ def upload(self, contents, file_name, mime_content_type=None, content_length=Non content_length=content_length, progress_listener=progress_listener) if upload_response.status_code == 200: new_file = B2File(connector=self.connector, parent_list=self, **upload_response.json()) + # Update file list after upload + self._update_files_list() return new_file else: raise B2RequestError(decode_error(upload_response)) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4e30332 --- /dev/null +++ b/install.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo Building and installing $(pwd) + +echo "Clean build..." +/usr/bin/python3 setup.py clean --all && echo -e "OK\n" + +echo "Build..." +/usr/bin/python3 setup.py build && echo -e "OK\n" + +echo "Install" +sudo -H /usr/bin/python3 setup.py install && echo -e "OK\n" + From b2cc653bb602c4de9938f01746908b511cd543f8 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 20:11:55 +0800 Subject: [PATCH 07/19] file_list, b2_file: add get file versions functionality; tests.py: cleaned up tests --- b2blaze/api.py | 3 +- b2blaze/models/b2_file.py | 21 ++-- b2blaze/models/file_list.py | 85 +++++++++++-- tests.py | 230 ++++++++++++++++++++++++++---------- 4 files changed, 258 insertions(+), 81 deletions(-) diff --git a/b2blaze/api.py b/b2blaze/api.py index 441ff88..33758de 100644 --- a/b2blaze/api.py +++ b/b2blaze/api.py @@ -20,7 +20,8 @@ class BucketAPI(): delete = '/b2_delete_bucket' list_buckets = '/b2_list_buckets' list_files = '/b2_list_file_names' - upload = '/b2_get_upload_url' + list_file_versions = '/b2_list_file_versions' + upload_url = '/b2_get_upload_url' upload_large = '/b2_start_large_file' upload_large_part = '/b2_get_upload_part_url' upload_large_finish = '/b2_finish_large_file' diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index 0d08315..e69914b 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -53,11 +53,19 @@ def delete(self): response = self.connector.make_request(path=path, method='post', params=params) if response.status_code == 200: self.deleted = True - del self.parent_list._files_by_name[self.file_name] - del self.parent_list._files_by_id[self.file_id] + # Delete from parent list if exists + self.parent_list._files_by_name.pop(self.file_name) + self.parent_list._files_by_id.pop(self.file_id) else: raise B2RequestError(decode_error(response)) - #TODO: Raise Error + + def versions(self): + """ Return list of all versions of the current file. """ + data = self.parent_list.file_versions_by_id(self.file_id, self.file_name) + if 'file_versions' in data: + return data['file_versions'] + return None + def delete_version(self): """ Delete a file version (Does not delete entire file history: only most recent version) @@ -70,13 +78,8 @@ def delete_version(self): 'fileName': b2_url_encode(self.file_name) } response = self.connector.make_request(path=path, method='post', params=params) - if response.status_code == 200: - self.deleted = True - del self.parent_list._files_by_name[self.file_name] - del self.parent_list._files_by_id[self.file_id] - else: + if not response.status_code == 200: raise B2RequestError(decode_error(response)) - #TODO: Raise Error def download(self): """ diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index 842223c..f2707be 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -7,6 +7,7 @@ from ..utilities import b2_url_encode, get_content_length, get_part_ranges, decode_error, RangeStream, StreamWithHashProgress from ..b2_exceptions import B2RequestError, B2FileNotFound from multiprocessing.dummy import Pool as ThreadPool +from ..api import BucketAPI, FileAPI class B2FileList(object): """ @@ -36,7 +37,7 @@ def _update_files_list(self, retrieve=False): :param retrieve: :return: """ - path = '/b2_list_file_names' + path = BucketAPI.list_files files = [] new_files_to_retrieve = True params = { @@ -63,6 +64,76 @@ def _update_files_list(self, retrieve=False): if retrieve: return files + def file_versions_by_id(self, file_id, file_name): + """ Return all the versions of all files in a given bucket. + Returns dict: + 'file_name': (str) Filename + 'file_id': (str) File ID + """ + return self._get_file_versions(file_id, file_name) + + + def all_file_versions(self): + """ Return all the versions of all files in a given bucket. + Returns dict: + 'file_names': (list) String filenames + 'file_versions': (list) b2blaze File objects + """ + return self._get_file_versions() + + def _get_file_versions(self, file_id=None, file_name=None): + """ Internal method. Return all the versions of all files in a given bucket, or single version history. + + Params: + file_id: (str) File id (optional, required if requesting single file versions) + file_name: (str) File id (optional, required if requesting single file versions) + + Returns dict: + 'file_names': (list) String filenames + 'file_versions': (list) b2blaze File objects + """ + + path = BucketAPI.list_file_versions + file_versions = dict() + file_names = [] + new_files_to_retrieve = True + params = { + 'bucketId': self.bucket.bucket_id, + 'maxFileCount': 10000 + } + + # If specified file ID, we only want files which match + if file_id: + params['startFileId'] = file_id + params['startFileName'] = file_name + params['maxFileCount'] = 1 + + while new_files_to_retrieve: + if file_id: new_files_to_retrieve = False # Avoid infinite loops destroying your API limit :) + + response = self.connector.make_request(path=path, method='post', params=params) + if response.status_code == 200: + files_json = response.json() + for file_json in files_json['files']: + new_file = B2File(connector=self.connector, parent_list=self, **file_json) + file_name, file_id = file_json['fileName'], file_json['fileId'] + file_names.append(file_name) + + # Add file to list keyed by file_id + if file_id in file_versions: + file_versions[file_id].append(new_file) + else: + file_versions[file_id] = [new_file] + + if files_json['nextFileName'] is None: + new_files_to_retrieve = False + else: + params['startFileName'] = files_json['nextFileName'] + else: + raise B2RequestError(decode_error(response)) + return {'file_names': file_names, 'file_versions': file_versions} + + def get(self, file_name=None, file_id=None): """ @@ -71,7 +142,7 @@ def get(self, file_name=None, file_id=None): :return: """ if file_name is not None: - path = '/b2_list_file_names' + path = BucketAPI.list_files params = { 'prefix': b2_url_encode(file_name), 'bucketId': self.bucket.bucket_id @@ -87,7 +158,7 @@ def get(self, file_name=None, file_id=None): else: raise B2RequestError(decode_error(response)) elif file_id is not None: - path = '/b2_get_file_info' + path = FileAPI.file_info params = { 'fileId': file_id } @@ -113,7 +184,7 @@ def upload(self, contents, file_name, mime_content_type=None, content_length=Non """ if file_name[0] == '/': file_name = file_name[1:] - get_upload_url_path = '/b2_get_upload_url' + get_upload_url_path = BucketAPI.upload_url params = { 'bucketId': self.bucket.bucket_id } @@ -153,7 +224,7 @@ def upload_large_file(self, contents, file_name, part_size=None, num_threads=4, part_size = self.connector.recommended_part_size if content_length == None: content_length = get_content_length(contents) - start_large_file_path = '/b2_start_large_file' + start_large_file_path = BucketAPI.upload_large params = { 'bucketId': self.bucket.bucket_id, 'fileName': b2_url_encode(file_name), @@ -162,7 +233,7 @@ def upload_large_file(self, contents, file_name, part_size=None, num_threads=4, large_file_response = self.connector.make_request(path=start_large_file_path, method='post', params=params) if large_file_response.status_code == 200: file_id = large_file_response.json().get('fileId', None) - get_upload_part_url_path = '/b2_get_upload_part_url' + get_upload_part_url_path = BucketAPI.upload_large_part params = { 'fileId': file_id } @@ -189,7 +260,7 @@ def upload_part_worker(args): sha_list = pool.map(upload_part_worker, enumerate(get_part_ranges(content_length, part_size), 1)) pool.close() pool.join() - finish_large_file_path = '/b2_finish_large_file' + finish_large_file_path = BucketAPI.upload_large_finish params = { 'fileId': file_id, 'partSha1Array': sha_list diff --git a/tests.py b/tests.py index b793e67..7303d48 100644 --- a/tests.py +++ b/tests.py @@ -24,69 +24,117 @@ def setup_class(cls): print(cls.bucket_name) def test_create_b2_instance(self): - """ - - :return: None - """ + """Create a B2 instance """ b2 = b2blaze.b2lib.B2() + @pytest.mark.bucket + @pytest.mark.files def test_create_bucket(self): - """ - - :return: None - """ + """ Create a bucket by name. """ self.bucket = self.b2.buckets.create(self.bucket_name, security=self.b2.buckets.public) + assert self.bucket - def test_create_file_and_retrieve_by_id(self): - """ + @pytest.mark.bucket + @pytest.mark.files + def test_get_bucket(self): + """ Get a bucket by name """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + assert bucket - :return: None - """ + @pytest.mark.bucket + def test_get_all_buckets(self): + """ Get buckets. Number of buckets returned should be >1 """ + buckets = self.b2.buckets.all() + expect(len(buckets)).should.be.greater_than(1) + + @pytest.mark.bucket + @pytest.mark.files + def test_get_nonexistent_bucket(self): + """ Get a bucket which doesn't exist should return None """ + bucket = self.b2.buckets.get(bucket_name='this doesnt exist') + assert not bucket + + @pytest.mark.files + def test_create_file_and_retrieve_by_id(self): + """ Create a file and retrieve by ID """ bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - file = bucket.files.upload(contents='Hello World!', file_name='test/hello.txt') + contents='Hello World!'.encode('utf-8') # These fail unless encoded to UTF8 + file = bucket.files.upload(contents=contents, file_name='test/hello.txt') file2 = bucket.files.get(file_id=file.file_id) - def test_create_z_binary_file(self): - """ - :return: - """ - #from time import sleep + @pytest.mark.files + def test_direct_upload_file(self): + """ Upload binary file """ bucket = self.b2.buckets.get(bucket_name=self.bucket_name) binary_file = open('b2blaze/test_pic.jpg', 'rb') - uploaded_file = bucket.files.upload(contents=binary_file.read(), file_name='test_pic.jpg') + uploaded_file = bucket.files.upload(contents=binary_file, file_name='test_pic2.jpg') binary_file.close() - #sleep(3) - #downloaded_file = uploaded_file.download() - #save_file = open('save_pic.jpg', 'wb') - #save_file.write(downloaded_file.read()) - #save_file.close() - def test_direct_upload_file(self): - """ - :return: - """ - #from time import sleep + @pytest.mark.files + def test_get_all_files(self): + """ Get all files from a bucket """ bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - binary_file = open('b2blaze/test_pic.jpg', 'rb') - uploaded_file = bucket.files.upload(contents=binary_file, file_name='test_pic2.jpg') - binary_file.close() + files = bucket.files.all() + print('test_get_files: all files: ', len(files)) - def test_download_file(self): - """ - :return: None - """ + @pytest.mark.files + def test_get_all_file_versions(self): + """ Get all file versions from a bucket """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + files = bucket.files.all_file_versions() + print('test_get_all_file_versions: all versions: ', len(files['file_versions'])) + assert len(files['file_versions']) > 0, 'File versions should exist' + + + @pytest.mark.files + def test_get_file_by_name(self): + """ Get file by name """ bucket = self.b2.buckets.get(bucket_name=self.bucket_name) file = bucket.files.get(file_name='test/hello.txt') - file.download() + assert file - def test_download_url(self): - """ - :return: None - """ + @pytest.mark.files + def test_get_file_by_id(self): + """ Get file by id """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + file = bucket.files.get(file_name='test/hello.txt') + assert file + + + @pytest.mark.files + def test_get_file_versions(self): + """ Get all versions of a file """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + file = bucket.files.get(file_name='test/hello.txt') + versions = file.versions() + assert len(versions) > 0, 'File should have multiple versions' + + + @pytest.mark.files + def test_get_file_doesnt_exist(self): + """ Get file which doens't exist should raise B2FileNotFound """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + with pytest.raises(B2FileNotFound): + file = bucket.files.get(file_name='nope.txt') + with pytest.raises(B2RequestError): + file2 = bucket.files.get(file_id='abcd') + + @pytest.mark.files + def test_download_file(self): + """ Get file by id """ + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + file = bucket.files.get(file_name='test/hello.txt') + data = file.download() + assert len(data.read()) > 0 + + + @pytest.mark.files + def test_download_url(self): + """ Download file url should be publicly GET accessible """ import requests bucket = self.b2.buckets.get(bucket_name=self.bucket_name) file = bucket.files.get(file_name='test/hello.txt') @@ -96,41 +144,95 @@ def test_download_url(self): print(downloaded_file.json()) raise ValueError - def test_get_buckets(self): - """ - :return: None + @pytest.mark.files + def test_delete_file(self): + """ Should create + upload, then delete a file by name. + File should no longer exist when searched by name in bucket. """ - buckets = self.b2.buckets.all() - expect(len(buckets)).should.be.greater_than(1) + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - def test_get_files(self): - """ + # Upload file & delete + contents='Delete this'.encode('utf-8') # These fail unless encoded to UTF8 + upload = bucket.files.upload(contents=contents, file_name='test/deleteme.txt') + print('test_delete_file: upload.file_name', upload.file_name) + print('test_delete_file: upload.file_id', upload.file_id) + upload.delete() - :return: None - """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - files = bucket.files.all() + # Refresh bucket; getting the the file should fail + with pytest.raises(B2FileNotFound): + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + file = bucket.files.get(file_name=upload.file_name) + assert not file, 'Deleted file should not exist' - def test_get_file_doesnt_exist(self): - """ - :return: - """ + @pytest.mark.files + def test_delete_file_version(self): + """ Delete a file version by name. It should still exist when searched.""" bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + + # Upload file & delete + file = bucket.files.get(file_name='test/hello.txt') + assert file, 'File should exist' + + print('test_delete_file_version: file_name', file.file_name) + print('test_delete_file_version: file_id', file.file_id) + file.delete_version() + + # Refresh bucket; getting the the file should fail with pytest.raises(B2FileNotFound): - file = bucket.files.get(file_name='nope.txt') - with pytest.raises(B2RequestError): - file2 = bucket.files.get(file_id='abcd') + bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + file2 = bucket.files.get(file_name=file.file_name) + assert file2, 'Deleted file version only, file should still exist' + + + @pytest.mark.bucket + def test_delete_non_empty_bucket(self): + """ Delete bucket should fail on bucket non-empty """ + self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - def test_z_delete_bucket(self): - """ + # Upload file + self.bucket.files.upload(contents='Hello World!'.encode('utf-8'), file_name='test/hello.txt') + assert len(self.bucket.files.all()) > 0, "Bucket should still contain files" + + # Should raise B2RequestError on non-empty + with pytest.raises(B2RequestError): + self.bucket.delete() + + # Bucket should still exist + assert self.b2.buckets.get(bucket_name=self.bucket_name), 'bucket should still exist' + + + @pytest.mark.bucket + @pytest.mark.files + def test_cleanup_bucket_files(self): + """ Delete all files from bucket. """ + self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + self.bucket.files.upload(contents='Hello World!'.encode('utf-8'), file_name='test/hello.txt') - :return: None - """ + files = self.bucket.files.all() + assert len(files) > 0, 'Bucket should still contain files' + for f in files: + f.delete() + assert len(self.bucket.files.all()) == 0, 'Bucket should be empty' + + + @pytest.mark.bucket + def test_delete_bucket(self): + """ Delete bucket """ self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + + # Ascertain it's empty + files_new = self.bucket.files.all() + print('files:', ', '.join([f.file_name for f in files_new])) + assert len(files_new) == 0, "Bucket should contain no files but contains {}".format(len(files_new)) + + # Delete self.bucket.delete() - #TODO: Assert cannot retrieve bucket by ID or name + + # Confirm bucket is gone. bucket.get() nonexistent should return None. + assert not self.b2.buckets.get(bucket_name=self.bucket_name), 'Deleted bucket still exists' + # def test_failure_to_create_bucket(self): # expect(self.b2.create_bucket( From de715ed6cf209cd1db961bf36719846fe43c1862 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 22:31:44 +0800 Subject: [PATCH 08/19] Clean up API structure --- b2blaze/__init__.py | 3 ++- b2blaze/api.py | 31 +++++++++---------------------- b2blaze/connector.py | 6 +++--- b2blaze/models/b2_file.py | 6 +++--- b2blaze/models/bucket.py | 4 ++-- b2blaze/models/bucket_list.py | 5 +++-- b2blaze/models/file_list.py | 18 +++++++++--------- 7 files changed, 31 insertions(+), 42 deletions(-) diff --git a/b2blaze/__init__.py b/b2blaze/__init__.py index dd65c78..cc61261 100644 --- a/b2blaze/__init__.py +++ b/b2blaze/__init__.py @@ -1 +1,2 @@ -from b2blaze.b2lib import B2 \ No newline at end of file +from b2blaze.b2lib import B2 +from .api import API_VERSION, BASE_URL, API \ No newline at end of file diff --git a/b2blaze/api.py b/b2blaze/api.py index 33758de..80ed85a 100644 --- a/b2blaze/api.py +++ b/b2blaze/api.py @@ -2,34 +2,21 @@ # BackBlaze API endpoints API_VERSION = '/b2api/v2' -BASE_API_URL = 'https://api.backblazeb2.com' + API_VERSION +BASE_URL = 'https://api.backblazeb2.com' + API_VERSION -class AuthAPI(): +class API(): authorize = '/b2_authorize_account' - -class FileAPI(): - delete = '/b2_hide_file' - delete_version = '/b2_delete_file_version' + delete_file = '/b2_hide_file' + delete_file_version = '/b2_delete_file_version' file_info = '/b2_get_file_info' - download_by_id = '/b2_download_file_by_id' - - -class BucketAPI(): - create = '/b2_create_bucket' - delete = '/b2_delete_bucket' - list_buckets = '/b2_list_buckets' - list_files = '/b2_list_file_names' + download_file_by_id = '/b2_download_file_by_id' + list_all_files = '/b2_list_file_names' list_file_versions = '/b2_list_file_versions' upload_url = '/b2_get_upload_url' upload_large = '/b2_start_large_file' upload_large_part = '/b2_get_upload_part_url' upload_large_finish = '/b2_finish_large_file' - - - -class AccountAPI(): - pass - -class ApplicationKeyAPI(): - pass \ No newline at end of file + create_bucket = '/b2_create_bucket' + delete_bucket = '/b2_delete_bucket' + list_all_buckets = '/b2_list_buckets' \ No newline at end of file diff --git a/b2blaze/connector.py b/b2blaze/connector.py index 3d9f4be..d8ff5d2 100644 --- a/b2blaze/connector.py +++ b/b2blaze/connector.py @@ -8,7 +8,7 @@ import sys from hashlib import sha1 from b2blaze.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress -from .api import BASE_API_URL, API_VERSION, AuthAPI, FileAPI +from .api import BASE_URL, API_VERSION, API class B2Connector(object): """ @@ -52,7 +52,7 @@ def _authorize(self): :return: """ - path = BASE_API_URL + AuthAPI.authorize + path = BASE_URL + API.authorize result = requests.get(path, auth=HTTPBasicAuth(self.key_id, self.application_key)) if result.status_code == 200: @@ -61,7 +61,7 @@ def _authorize(self): self.account_id = result_json['accountId'] self.auth_token = result_json['authorizationToken'] self.api_url = result_json['apiUrl'] + API_VERSION - self.download_url = result_json['downloadUrl'] + API_VERSION + FileAPI.download_by_id + self.download_url = result_json['downloadUrl'] + API_VERSION + API.download_file_by_id self.recommended_part_size = result_json['recommendedPartSize'] self.api_session = requests.Session() self.api_session.headers.update({ diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index e69914b..9c64568 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -4,7 +4,7 @@ from io import BytesIO from ..utilities import b2_url_encode, b2_url_decode, decode_error from ..b2_exceptions import B2RequestError -from ..api import FileAPI +from ..api import API class B2File(object): """ @@ -45,7 +45,7 @@ def delete(self): """ Soft-delete a file (hide it from files list, but previous versions are saved.) :return: """ - path = FileAPI.delete + path = API.delete params = { 'bucketId': self.parent_list.bucket.bucket_id, 'fileName': b2_url_encode(self.file_name) @@ -72,7 +72,7 @@ def delete_version(self): :return: """ - path = FileAPI.delete_version + path = API.delete_version params = { 'fileId': self.file_id, 'fileName': b2_url_encode(self.file_name) diff --git a/b2blaze/models/bucket.py b/b2blaze/models/bucket.py index d1ca1e3..27fc427 100644 --- a/b2blaze/models/bucket.py +++ b/b2blaze/models/bucket.py @@ -4,7 +4,7 @@ from .file_list import B2FileList from ..b2_exceptions import B2RequestError from ..utilities import decode_error - +from ..api import API class B2Bucket(object): """ @@ -42,7 +42,7 @@ def delete(self): :return: """ - path = '/b2_delete_bucket' + path = API.delete_bucket files = self.files.all() for file in files: file.delete() diff --git a/b2blaze/models/bucket_list.py b/b2blaze/models/bucket_list.py index e8eac2c..618a62f 100644 --- a/b2blaze/models/bucket_list.py +++ b/b2blaze/models/bucket_list.py @@ -5,6 +5,7 @@ from ..b2_exceptions import B2InvalidBucketName, B2InvalidBucketConfiguration, B2BucketCreationError, B2RequestError from ..utilities import decode_error from .bucket import B2Bucket +from ..api import API class B2Buckets(object): @@ -36,7 +37,7 @@ def _update_bucket_list(self, retrieve=False): :param retrieve: :return: """ - path = '/b2_list_buckets' + path = API.list_all_buckets response = self.connector.make_request(path=path, method='post', account_id_required=True) if response.status_code == 200: response_json = response.json() @@ -73,7 +74,7 @@ def create(self, bucket_name, security, configuration=None): :param configuration: :return: """ - path = '/b2_create_bucket' + path = API.create_bucket if type(bucket_name) != str and type(bucket_name) != bytes: raise B2InvalidBucketName if type(configuration) != dict and configuration is not None: diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index f2707be..c99441a 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -7,7 +7,7 @@ from ..utilities import b2_url_encode, get_content_length, get_part_ranges, decode_error, RangeStream, StreamWithHashProgress from ..b2_exceptions import B2RequestError, B2FileNotFound from multiprocessing.dummy import Pool as ThreadPool -from ..api import BucketAPI, FileAPI +from ..api import API class B2FileList(object): """ @@ -37,7 +37,7 @@ def _update_files_list(self, retrieve=False): :param retrieve: :return: """ - path = BucketAPI.list_files + path = API.list_all_files files = [] new_files_to_retrieve = True params = { @@ -93,7 +93,7 @@ def _get_file_versions(self, file_id=None, file_name=None): 'file_versions': (list) b2blaze File objects """ - path = BucketAPI.list_file_versions + path = API.list_file_versions file_versions = dict() file_names = [] new_files_to_retrieve = True @@ -142,7 +142,7 @@ def get(self, file_name=None, file_id=None): :return: """ if file_name is not None: - path = BucketAPI.list_files + path = API.list_all_files params = { 'prefix': b2_url_encode(file_name), 'bucketId': self.bucket.bucket_id @@ -158,7 +158,7 @@ def get(self, file_name=None, file_id=None): else: raise B2RequestError(decode_error(response)) elif file_id is not None: - path = FileAPI.file_info + path = API.file_info params = { 'fileId': file_id } @@ -184,7 +184,7 @@ def upload(self, contents, file_name, mime_content_type=None, content_length=Non """ if file_name[0] == '/': file_name = file_name[1:] - get_upload_url_path = BucketAPI.upload_url + get_upload_url_path = API.upload_url params = { 'bucketId': self.bucket.bucket_id } @@ -224,7 +224,7 @@ def upload_large_file(self, contents, file_name, part_size=None, num_threads=4, part_size = self.connector.recommended_part_size if content_length == None: content_length = get_content_length(contents) - start_large_file_path = BucketAPI.upload_large + start_large_file_path = API.upload_large params = { 'bucketId': self.bucket.bucket_id, 'fileName': b2_url_encode(file_name), @@ -233,7 +233,7 @@ def upload_large_file(self, contents, file_name, part_size=None, num_threads=4, large_file_response = self.connector.make_request(path=start_large_file_path, method='post', params=params) if large_file_response.status_code == 200: file_id = large_file_response.json().get('fileId', None) - get_upload_part_url_path = BucketAPI.upload_large_part + get_upload_part_url_path = API.upload_large_part params = { 'fileId': file_id } @@ -260,7 +260,7 @@ def upload_part_worker(args): sha_list = pool.map(upload_part_worker, enumerate(get_part_ranges(content_length, part_size), 1)) pool.close() pool.join() - finish_large_file_path = BucketAPI.upload_large_finish + finish_large_file_path = API.upload_large_finish params = { 'fileId': file_id, 'partSha1Array': sha_list From 2075129eea8a5357b5515d2a36013d79122c2507 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 24 Oct 2018 22:37:01 +0800 Subject: [PATCH 09/19] b2_file: adjust function docstrings --- b2blaze/models/b2_file.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index 9c64568..f2491bc 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -28,8 +28,8 @@ def __init__(self, connector, parent_list, fileId, fileName, contentSha1, conten :param kwargs: """ self.file_id = fileId - #self.file_name = b2_url_decode(fileName) - #TODO: Find out if this is necessary + # self.file_name_decoded = b2_url_decode(fileName) + #TODO: Find out if this is necessary self.file_name = fileName self.content_sha1 = contentSha1 self.content_length = contentLength @@ -42,10 +42,8 @@ def __init__(self, connector, parent_list, fileId, fileName, contentSha1, conten self.deleted = False def delete(self): - """ Soft-delete a file (hide it from files list, but previous versions are saved.) - :return: - """ - path = API.delete + """ Soft-delete a file (hide it from files list, but previous versions are saved.) """ + path = API.delete_file params = { 'bucketId': self.parent_list.bucket.bucket_id, 'fileName': b2_url_encode(self.file_name) @@ -68,11 +66,8 @@ def versions(self): def delete_version(self): - """ Delete a file version (Does not delete entire file history: only most recent version) - - :return: - """ - path = API.delete_version + """ Delete a file version (Does not delete entire file history: only most recent version) """ + path = API.delete_file_version params = { 'fileId': self.file_id, 'fileName': b2_url_encode(self.file_name) @@ -82,10 +77,7 @@ def delete_version(self): raise B2RequestError(decode_error(response)) def download(self): - """ - - :return: - """ + """ Download latest file version """ response = self.connector.download_file(file_id=self.file_id) if response.status_code == 200: return BytesIO(response.content) @@ -94,8 +86,5 @@ def download(self): @property def url(self): - """ - - :return: file download url - """ + """ Return file download URL """ return self.connector.download_url + '?fileId=' + self.file_id From 7a2c90e31cf7530527bdb809d070473d0f797478 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Fri, 26 Oct 2018 20:40:34 +0800 Subject: [PATCH 10/19] Major update: All methods working, unit tests all passing. file_list: Added include_hidden flag to all() method; delete_all_files method added tests.py: Unit tests all passing. Added main() method to run pytest. --- README.md | 13 +- b2blaze/models/b2_file.py | 73 +++++++++-- b2blaze/models/bucket.py | 25 ++-- b2blaze/models/file_list.py | 187 +++++++++++++++++---------- install.sh | 2 +- tests.py | 250 ++++++++++++++++++++++++++---------- 6 files changed, 394 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 0fc5db5..4abaf90 100644 --- a/README.md +++ b/README.md @@ -173,11 +173,22 @@ This deletes a single version of a file. (See the [docs on File Versions](https: #### Hide (aka "Soft-delete") a file ```python -file.delete() +file.hide() ``` This hides a file (aka "soft-delete") so that downloading by name will not find the file, but previous versions of the file are still stored. (See the [docs on Hiding file](https://www.backblaze.com/b2/docs/b2_hide_file.html) at Backblaze for details) +## Testing + +Unit testing with pytest +Before running, you must set the environment variables: `B2_KEY_ID` and `B2_APPLICATION_KEY` + +** Run tests ** + +``` bash +python3 ./tests.py +``` + ## LICENSE diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index f2491bc..8485700 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -41,7 +41,37 @@ def __init__(self, connector, parent_list, fileId, fileName, contentSha1, conten self.parent_list = parent_list self.deleted = False - def delete(self): + def get_versions(self, limit=None): + """ Fetch list of all versions of the current file. + Params: + limit: (int) Limit number of results returned (optional, default 10000) + + Returns: + file_versions (list) B2FileObject of all file versions + """ + bucket_id = self.parent_list.bucket.bucket_id + + path = API.list_file_versions + file_versions = [] + params = { + 'bucketId': bucket_id, + 'maxFileCount': limit or 10000, + 'startFileId': self.file_id, + 'startFileName': self.file_name, + } + + response = self.connector.make_request(path=path, method='post', params=params) + if response.status_code == 200: + files_json = response.json() + for file_json in files_json['files']: + new_file = B2File(connector=self.connector, parent_list=self, **file_json) + file_versions.append(new_file) + else: + raise B2RequestError(decode_error(response)) + return file_versions + + + def hide(self): """ Soft-delete a file (hide it from files list, but previous versions are saved.) """ path = API.delete_file params = { @@ -57,13 +87,38 @@ def delete(self): else: raise B2RequestError(decode_error(response)) - def versions(self): - """ Return list of all versions of the current file. """ - data = self.parent_list.file_versions_by_id(self.file_id, self.file_name) - if 'file_versions' in data: - return data['file_versions'] - return None - + + def delete_all_versions(self, confirm=False): + """ Delete completely all versions of a file. + ** NOTE THAT THIS CAN BE VERY EXPENSIVE IN TERMS OF YOUR API LIMITS ** + Each call to delete_all_versions will result in multiple API calls: + One API call per file version to be deleted, per file. + 1. Call '/b2_list_file_versions' to get file versions + 2. Call '/b2_delete_file_version' once for each version of the file + + This means: if you have 10 files with 50 versions each and call delete_all_versions, + you will spend (10 + 1) x 50 == 550 API calls against your BackBlaze b2 API limit. + + ** You have been warned! BE CAREFUL!!! ** + """ + print(self.delete_all_versions.__name__, self.delete_all_versions.__doc__) # Print warnings at call time. + + # Confirm deletion + if not confirm: + print('To call this function, use delete_all_versions(confirm=True)') + return False + + versions = self.get_versions() + + version_count = len(versions) + if not version_count > 0: + print('No file versions') + else: + print(version_count, 'file versions') + for count, v in enumerate(versions): + print('deleting [{}/{}]'.format(count + 1 , version_count)) + v.delete_version() + def delete_version(self): """ Delete a file version (Does not delete entire file history: only most recent version) """ @@ -75,6 +130,8 @@ def delete_version(self): response = self.connector.make_request(path=path, method='post', params=params) if not response.status_code == 200: raise B2RequestError(decode_error(response)) + self.deleted = True + def download(self): """ Download latest file version """ diff --git a/b2blaze/models/bucket.py b/b2blaze/models/bucket.py index 27fc427..befcc24 100644 --- a/b2blaze/models/bucket.py +++ b/b2blaze/models/bucket.py @@ -37,15 +37,23 @@ def __init__(self, connector, parent_list, bucketId, bucketName, bucketType, buc self.parent_list = parent_list self.deleted = False - def delete(self): - """ + def delete(self, delete_files=False, confirm_non_empty=False): + """ Delete a bucket. - :return: + Params: + delete_files: (bool) Delete all files first. + confirm_non_empty: (bool) Confirm deleting on bucket not empty. """ path = API.delete_bucket - files = self.files.all() - for file in files: - file.delete() + files = self.files.all(include_hidden=True) + if delete_files: + if not confirm_non_empty: + raise Exception('Bucket is not empty! Must confirm deletion of all files with confirm_non_empty=True') + else: + print("Deleting all files from bucket. Beware API limits!") + self.files.delete_all(confirm=True) + + params = { 'bucketId': self.bucket_id } @@ -63,8 +71,5 @@ def edit(self): @property def files(self): - """ - - :return: - """ + """ List of files in the bucket. B2FileList instance. """ return B2FileList(connector=self.connector, bucket=self) \ No newline at end of file diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index c99441a..00e7422 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -24,25 +24,56 @@ def __init__(self, connector, bucket): self._files_by_name = {} self._files_by_id = {} - def all(self): - """ + def all(self, include_hidden=False, limit=None): + """ Return an updated list of all files. + (This does not include hidden files unless include_hidden flag set to True) - :return: - """ - return self._update_files_list(retrieve=True) + Parameters: + include_hidden: (bool) Include hidden files + limit: (int) Limit number of file results - def _update_files_list(self, retrieve=False): """ + if not include_hidden: + return self._update_files_list(retrieve=True, limit=limit) + else: + results = self.all_file_versions(limit=limit) + versions = results['file_versions'] + file_ids = results['file_ids'] + if versions: + # Return only the first file from a given file with multiple versions + files = [versions[f][0] for f in file_ids] + return files + return [] # Return empty set on no results - :param retrieve: - :return: + def delete_all(self, confirm=True): + """ Delete all files in the bucket. + Parameters: + confirm: (bool) Safety check. Confirm deletion + """ + if not confirm: + raise Exception('This will delete all files! Pass confirm=True') + + all_files = self.all(include_hidden=True) + try: + for f in all_files: + f.delete_all_versions(confirm=True) + except Exception as E: + raise B2RequestError(decode_error(E)) + return [] + + + def _update_files_list(self, retrieve=False, limit=None): + """ Retrieve list of all files in bucket + Parameters: + limit: (int) Max number of file results, default 10000 + retrieve: (bool) Refresh local store. (default: false) """ path = API.list_all_files files = [] new_files_to_retrieve = True params = { 'bucketId': self.bucket.bucket_id, - 'maxFileCount': 10000 + 'maxFileCount': limit or 10000 } while new_files_to_retrieve: response = self.connector.make_request(path=path, method='post', params=params) @@ -64,60 +95,88 @@ def _update_files_list(self, retrieve=False): if retrieve: return files - def file_versions_by_id(self, file_id, file_name): - """ Return all the versions of all files in a given bucket. - Returns dict: - 'file_name': (str) Filename - 'file_id': (str) File ID - """ - return self._get_file_versions(file_id, file_name) + def get(self, file_name=None, file_id=None): + """ Get a file by file name or id. + Required: + file_name or file_id - def all_file_versions(self): - """ Return all the versions of all files in a given bucket. - Returns dict: - 'file_names': (list) String filenames - 'file_versions': (list) b2blaze File objects + Parameters: + file_name: (str) File name + file_id: (str) File ID + """ + if file_name: + file = self._get_by_name(file_name) + + elif file_id: + file = self._get_by_id(file_id) + else: + raise ValueError('file_name or file_id must be passed') + + return file + + + def get_versions(self, file_name=None, file_id=None, limit=None): + """ Return list of all the versions of one file in current bucket. + Required: + file_id or file_name (either) + + Params: + file_id: (str) File id + file_name: (str) File id + limit: (int) Limit number of results returned (optional) + + Returns: + file_versions (list) B2FileObject of all file versions """ - return self._get_file_versions() + if file_name: + file = self.get(file_name) - def _get_file_versions(self, file_id=None, file_name=None): - """ Internal method. Return all the versions of all files in a given bucket, or single version history. + elif file_id: + file = self.get(file_id=file_id) + else: + raise ValueError('Either file_id or file_name required for get_versions') + return file.get_versions() + + + def all_file_versions(self, limit=None): + """ Return all the versions of all files in a given bucket. Params: - file_id: (str) File id (optional, required if requesting single file versions) - file_name: (str) File id (optional, required if requesting single file versions) + limit: (int) Limit number of results returned (optional). Defaults to 10000 Returns dict: 'file_names': (list) String filenames - 'file_versions': (list) b2blaze File objects + 'file_ids': (list) File IDs + 'file_versions': (dict) b2blaze File objects, keyed by file name """ path = API.list_file_versions file_versions = dict() file_names = [] + file_ids = [] new_files_to_retrieve = True params = { 'bucketId': self.bucket.bucket_id, 'maxFileCount': 10000 } - # If specified file ID, we only want files which match - if file_id: - params['startFileId'] = file_id - params['startFileName'] = file_name - params['maxFileCount'] = 1 + # Limit files + if limit: + params['maxFileCount'] = limit while new_files_to_retrieve: - if file_id: new_files_to_retrieve = False # Avoid infinite loops destroying your API limit :) response = self.connector.make_request(path=path, method='post', params=params) if response.status_code == 200: files_json = response.json() for file_json in files_json['files']: new_file = B2File(connector=self.connector, parent_list=self, **file_json) + + # Append file_id, file_name to lists file_name, file_id = file_json['fileName'], file_json['fileId'] file_names.append(file_name) + file_ids.append(file_id) # Add file to list keyed by file_id if file_id in file_versions: @@ -131,46 +190,40 @@ def _get_file_versions(self, file_id=None, file_name=None): params['startFileName'] = files_json['nextFileName'] else: raise B2RequestError(decode_error(response)) - return {'file_names': file_names, 'file_versions': file_versions} + return {'file_names': file_names, 'file_versions': file_versions, 'file_ids': file_ids} - def get(self, file_name=None, file_id=None): - """ - - :param file_name: - :param file_id: - :return: - """ - if file_name is not None: - path = API.list_all_files - params = { - 'prefix': b2_url_encode(file_name), - 'bucketId': self.bucket.bucket_id - } + def _get_by_name(self, file_name): + """ Internal method, return file by file name """ + path = API.list_all_files + params = { + 'prefix': b2_url_encode(file_name), + 'bucketId': self.bucket.bucket_id + } - response = self.connector.make_request(path, method='post', params=params) - if response.status_code == 200: - file_json = response.json() - if len(file_json['files']) > 0: - return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) - else: - raise B2FileNotFound('fileName - ' + file_name) - else: - raise B2RequestError(decode_error(response)) - elif file_id is not None: - path = API.file_info - params = { - 'fileId': file_id - } - response = self.connector.make_request(path, method='post', params=params) - if response.status_code == 200: - file_json = response.json() - return B2File(connector=self.connector, parent_list=self, **file_json) + response = self.connector.make_request(path, method='post', params=params) + if response.status_code == 200: + file_json = response.json() + if len(file_json['files']) > 0: + return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) else: - raise B2RequestError(decode_error(response)) + raise B2FileNotFound('fileName - ' + file_name) else: - raise ValueError('file_name or file_id must be passed') + raise B2RequestError(decode_error(response)) + def _get_by_id(self, file_id): + """ Internal method, return file by file id """ + path = API.file_info + params = { + 'fileId': file_id + } + response = self.connector.make_request(path, method='post', params=params) + if response.status_code == 200: + file_json = response.json() + return B2File(connector=self.connector, parent_list=self, **file_json) + else: + raise B2RequestError(decode_error(response)) + def upload(self, contents, file_name, mime_content_type=None, content_length=None, progress_listener=None): """ diff --git a/install.sh b/install.sh index 4e30332..c8c1cf7 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,4 @@ #!/bin/bash - echo Building and installing $(pwd) echo "Clean build..." @@ -9,5 +8,6 @@ echo "Build..." /usr/bin/python3 setup.py build && echo -e "OK\n" echo "Install" +sudo echo "Enter sudo password for install" sudo -H /usr/bin/python3 setup.py install && echo -e "OK\n" diff --git a/tests.py b/tests.py index 7303d48..061ba5c 100644 --- a/tests.py +++ b/tests.py @@ -4,15 +4,13 @@ import b2blaze.b2lib import sure from sure import expect -import random -import string +from datetime import datetime import pytest from b2blaze.b2_exceptions import B2RequestError, B2FileNotFound class TestB2(object): - """ + """ Tests for the b2blaze library """ - """ @classmethod def setup_class(cls): """ @@ -20,25 +18,52 @@ def setup_class(cls): :return: None """ cls.b2 = b2blaze.b2lib.B2() - cls.bucket_name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in range(7)) - print(cls.bucket_name) + timestamp = datetime.now().strftime('%Y-%m-%d-%H%M%S') + cls.bucket_name = 'testbucket-' + timestamp + print('test bucket: ', cls.bucket_name) + # Helper methods def test_create_b2_instance(self): """Create a B2 instance """ b2 = b2blaze.b2lib.B2() + + @classmethod + def create_bucket(cls): + return cls.b2.buckets.create(cls.bucket_name, security=cls.b2.buckets.public) + + @classmethod + def getbucket(cls): + return cls.b2.buckets.get(bucket_name=cls.bucket_name) or cls.create_bucket() + + @classmethod + def upload_textfile(cls, contents="hello there", file_name='test/hello.txt'): + """ Upload text file with name 'test/hello.txt' """ + contents=contents.encode('utf-8') # These fail unless encoded to UTF8 + bucket = cls.getbucket() + return bucket.files.upload(contents=contents, file_name=file_name) + + + @classmethod + def is_b2_file(cls, obj): + """ hacky method for checking object class/type is B2File""" + if 'B2File' in str(type(obj)): + return True + return False + + ## Tests ## @pytest.mark.bucket @pytest.mark.files + @pytest.mark.versions def test_create_bucket(self): """ Create a bucket by name. """ self.bucket = self.b2.buckets.create(self.bucket_name, security=self.b2.buckets.public) assert self.bucket @pytest.mark.bucket - @pytest.mark.files def test_get_bucket(self): """ Get a bucket by name """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() assert bucket @pytest.mark.bucket @@ -48,7 +73,6 @@ def test_get_all_buckets(self): expect(len(buckets)).should.be.greater_than(1) @pytest.mark.bucket - @pytest.mark.files def test_get_nonexistent_bucket(self): """ Get a bucket which doesn't exist should return None """ bucket = self.b2.buckets.get(bucket_name='this doesnt exist') @@ -57,33 +81,43 @@ def test_get_nonexistent_bucket(self): @pytest.mark.files def test_create_file_and_retrieve_by_id(self): """ Create a file and retrieve by ID """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() contents='Hello World!'.encode('utf-8') # These fail unless encoded to UTF8 file = bucket.files.upload(contents=contents, file_name='test/hello.txt') file2 = bucket.files.get(file_id=file.file_id) + # It should be a B2File + assert self.is_b2_file(file2), 'Should be a B2File object' + @pytest.mark.files def test_direct_upload_file(self): """ Upload binary file """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() binary_file = open('b2blaze/test_pic.jpg', 'rb') uploaded_file = bucket.files.upload(contents=binary_file, file_name='test_pic2.jpg') binary_file.close() + assert self.is_b2_file(uploaded_file) @pytest.mark.files def test_get_all_files(self): - """ Get all files from a bucket """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + """ Get all files from a bucket. Returned objects are B2Files """ + bucket = self.getbucket() files = bucket.files.all() print('test_get_files: all files: ', len(files)) + # check type + assert self.is_b2_file(files[0]), 'Should be a B2File object' + + + @pytest.mark.versions @pytest.mark.files def test_get_all_file_versions(self): """ Get all file versions from a bucket """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() + file = self.upload_textfile() files = bucket.files.all_file_versions() print('test_get_all_file_versions: all versions: ', len(files['file_versions'])) assert len(files['file_versions']) > 0, 'File versions should exist' @@ -92,42 +126,78 @@ def test_get_all_file_versions(self): @pytest.mark.files def test_get_file_by_name(self): """ Get file by name """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - file = bucket.files.get(file_name='test/hello.txt') - assert file + bucket = self.getbucket() + file = self.upload_textfile() + + # check type + assert self.is_b2_file(file), 'Should be a B2File object' @pytest.mark.files def test_get_file_by_id(self): """ Get file by id """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - file = bucket.files.get(file_name='test/hello.txt') - assert file + bucket = self.getbucket() + file = self.upload_textfile() + # check type + assert self.is_b2_file(file), 'Should be a B2File object' + + @pytest.mark.versions @pytest.mark.files def test_get_file_versions(self): - """ Get all versions of a file """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + """ Get all versions of a file via the file.get_versions method. + Returned data should be a list, and items should be of type B2File + """ + bucket = self.getbucket() file = bucket.files.get(file_name='test/hello.txt') - versions = file.versions() + versions = file.get_versions() assert len(versions) > 0, 'File should have multiple versions' + + # check type + assert self.is_b2_file(versions[0]), 'Should be a B2File object' + + + @pytest.mark.versions + @pytest.mark.files + def test_bucket_get_file_versions_by_name(self): + """ Get all versions of a file by name file_list.get_versions method. + Returned data should be a list, and items should be of type B2File + """ + bucket = self.getbucket() + versions = bucket.files.get_versions(file_name='test/hello.txt') + assert len(versions) > 0, 'File should have multiple versions' + assert self.is_b2_file(versions[0]), 'Should be a B2File object' + + + @pytest.mark.versions + @pytest.mark.files + def test_bucket_get_file_versions_by_id(self): + """ Get all versions of a file by id file_list.get_versions method. + Returned data should be a list, and items should be of type B2File + """ + bucket = self.getbucket() + file = bucket.files.get(file_name='test/hello.txt') + versions = bucket.files.get_versions(file_id=file.file_id) + assert len(versions) > 0, 'File should have multiple versions' + assert self.is_b2_file(versions[0]), 'Should be a B2File object' @pytest.mark.files def test_get_file_doesnt_exist(self): """ Get file which doens't exist should raise B2FileNotFound """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() with pytest.raises(B2FileNotFound): file = bucket.files.get(file_name='nope.txt') with pytest.raises(B2RequestError): file2 = bucket.files.get(file_id='abcd') + @pytest.mark.files def test_download_file(self): """ Get file by id """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - file = bucket.files.get(file_name='test/hello.txt') + bucket = self.getbucket() + file = self.upload_textfile() data = file.download() assert len(data.read()) > 0 @@ -136,8 +206,8 @@ def test_download_file(self): def test_download_url(self): """ Download file url should be publicly GET accessible """ import requests - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - file = bucket.files.get(file_name='test/hello.txt') + bucket = self.getbucket() + file = self.upload_textfile() url = file.url downloaded_file = requests.get(url) if downloaded_file.status_code != 200: @@ -146,95 +216,137 @@ def test_download_url(self): @pytest.mark.files - def test_delete_file(self): - """ Should create + upload, then delete a file by name. - File should no longer exist when searched by name in bucket. + def test_hide_file(self): + """ Should create + upload, then hide / soft-delete a file by name. + File should no longer be returned when searched by name in bucket. """ - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - - # Upload file & delete - contents='Delete this'.encode('utf-8') # These fail unless encoded to UTF8 - upload = bucket.files.upload(contents=contents, file_name='test/deleteme.txt') - print('test_delete_file: upload.file_name', upload.file_name) + bucket = self.getbucket() + upload = self.upload_textfile(contents='Delete this', file_name='test/deleteme.txt') + + # Delete print('test_delete_file: upload.file_id', upload.file_id) - upload.delete() + print('test_delete_file: upload.file_name', upload.file_name) + upload.hide() # Refresh bucket; getting the the file should fail with pytest.raises(B2FileNotFound): - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() file = bucket.files.get(file_name=upload.file_name) - assert not file, 'Deleted file should not exist' + assert not file, 'Deleted file should not be in files list' @pytest.mark.files + @pytest.mark.versions def test_delete_file_version(self): """ Delete a file version by name. It should still exist when searched.""" - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() # Upload file & delete - file = bucket.files.get(file_name='test/hello.txt') - assert file, 'File should exist' + file = self.upload_textfile() + file2 = self.upload_textfile() + + # Update versions + versions = file.get_versions() + assert len(versions) > 1, 'File should should have multiple version' + + # Delete version print('test_delete_file_version: file_name', file.file_name) print('test_delete_file_version: file_id', file.file_id) file.delete_version() + # Refresh bucket; getting the the file should fail + file2 = bucket.files.get(file_name=file.file_name) + assert file2, 'Deleted file version only, file should still exist' + assert self.is_b2_file(file2), 'Should be a B2File object' + + + # @pytest.mark.versions + @pytest.mark.files + def test_delete_all_file_versions(self): + """ Delete all versions of a file. It should be gone completely from bucket.""" + bucket = self.getbucket() + + # Create file, make sure we have multiple versions + contents='Hello World!'.encode('utf-8') # These fail unless encoded to UTF8 + upload = bucket.files.upload(contents=contents, file_name='test/hello.txt') + + # Get + # versions = bucket.files.get_versions(file_name='test/hello.txt') + file = bucket.files.get(file_name='test/hello.txt') + versions = file.get_versions() + assert len(versions) > 0, 'File should should have multiple version' + + # Delete + print('test_delete_all_file_versions: file_name', file.file_name) + print('test_delete_all_file_versions: file_id', file.file_id) + file.delete_all_versions(confirm=True) + # Refresh bucket; getting the the file should fail with pytest.raises(B2FileNotFound): - bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() file2 = bucket.files.get(file_name=file.file_name) - assert file2, 'Deleted file version only, file should still exist' + assert not file2, 'Deleted all file versions, file should not exist' @pytest.mark.bucket def test_delete_non_empty_bucket(self): """ Delete bucket should fail on bucket non-empty """ - self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + bucket = self.getbucket() # Upload file - self.bucket.files.upload(contents='Hello World!'.encode('utf-8'), file_name='test/hello.txt') - assert len(self.bucket.files.all()) > 0, "Bucket should still contain files" + self.upload_textfile() + assert len(bucket.files.all()) > 0, "Bucket should still contain files" - # Should raise B2RequestError on non-empty + # Should raise exception on non-empty without confirm with pytest.raises(B2RequestError): - self.bucket.delete() - + bucket.delete() # Try to delete without confirmation + # Bucket should still exist - assert self.b2.buckets.get(bucket_name=self.bucket_name), 'bucket should still exist' + assert self.b2.buckets.get(bucket_name=bucket.bucket_name), 'bucket should still exist' + + # # Delete with confirmation + # bucket.delete(delete_files=True, confirm_non_empty=True) + + # # Bucket should be gone + # assert self.b2.buckets.get(bucket_name=bucket.bucket_name), 'bucket should not exist' @pytest.mark.bucket @pytest.mark.files - def test_cleanup_bucket_files(self): + @pytest.mark.versions + def test_bucket_delete_all_files(self): """ Delete all files from bucket. """ - self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) - self.bucket.files.upload(contents='Hello World!'.encode('utf-8'), file_name='test/hello.txt') + bucket = self.getbucket() + self.upload_textfile() - files = self.bucket.files.all() + files = bucket.files.all() assert len(files) > 0, 'Bucket should still contain files' - for f in files: - f.delete() - assert len(self.bucket.files.all()) == 0, 'Bucket should be empty' + + # Delete all files + bucket.files.delete_all() + assert len(bucket.files.all()) == 0, 'Bucket should be empty' @pytest.mark.bucket def test_delete_bucket(self): - """ Delete bucket """ - self.bucket = self.b2.buckets.get(bucket_name=self.bucket_name) + """ Delete empty bucket""" + bucket = self.getbucket() # Ascertain it's empty - files_new = self.bucket.files.all() - print('files:', ', '.join([f.file_name for f in files_new])) + files_new = bucket.files.all(include_hidden=True) assert len(files_new) == 0, "Bucket should contain no files but contains {}".format(len(files_new)) # Delete - self.bucket.delete() + bucket.delete() # Confirm bucket is gone. bucket.get() nonexistent should return None. - assert not self.b2.buckets.get(bucket_name=self.bucket_name), 'Deleted bucket still exists' + assert not self.b2.buckets.get(bucket_name=bucket.bucket_name), 'Deleted bucket still exists' +def main(): + import pytest + pytest_args = [ __file__, '--verbose'] + pytest.main(pytest_args) - # def test_failure_to_create_bucket(self): - # expect(self.b2.create_bucket( - # ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) - # for _ in range(4)))).should.have.raised(Exception) +if __name__ == '__main__': + main() \ No newline at end of file From 5457a706da579998dc199f118d044c21011b8124 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Sat, 27 Oct 2018 17:50:35 +0800 Subject: [PATCH 11/19] b2_exceptions: added errors for all API error codes --- b2blaze/b2_exceptions.py | 103 ++++++++++++++++++++++++++-------- b2blaze/b2lib.py | 4 +- b2blaze/connector.py | 4 +- b2blaze/models/b2_file.py | 10 ++-- b2blaze/models/bucket.py | 7 +-- b2blaze/models/bucket_list.py | 9 ++- b2blaze/models/file_list.py | 34 +++++------ tests.py | 12 ++-- 8 files changed, 117 insertions(+), 66 deletions(-) diff --git a/b2blaze/b2_exceptions.py b/b2blaze/b2_exceptions.py index 6cd00ae..d229757 100644 --- a/b2blaze/b2_exceptions.py +++ b/b2blaze/b2_exceptions.py @@ -1,53 +1,110 @@ """ Copyright George Sibble 2018 """ -class B2ApplicationKeyNotSet(Exception): - """ - """ +import json + +class B2ApplicationKeyNotSet(Exception): + """ You must set the B2_KEY_ID environment variable before running the application """ pass + class B2KeyIDNotSet(Exception): - """ + """ You must set the B2_APPLICATION_KEY environment variable before running the application """ + pass - """ + +class B2Exception(Exception): + """ Base exception class for the Backblaze API """ + + @staticmethod + def parse(response): + """ Parse the response error code and return the related error type. """ + response_json = response.json() + message = response_json['message'] + code = response_json['code'] + status = int(response_json['status']) + + exception_map = { + 400 : B2RequestError, + 401 : B2UnauthorizedError, + 403 : B2ForbiddenError, + 404 : B2FileNotFoundError, + 408 : B2RequestTimeoutError, + 429 : B2TooManyRequestsError, + 500 : B2InternalError, + 503 : B2ServiceUnavailableError, + } + + # Return B2Exception if unrecognized status code + if not status in exception_map: + return B2Exception('{} - {}: {}'.format(status, code, message)) + + ErrorClass = exception_map[status] + return ErrorClass('{} - {}: {}'.format(status, code, message)) + +class B2FileNotFoundError(Exception): + """ 404 Not Found """ pass -class B2InvalidBucketName(Exception): - """ - """ +class B2RequestError(Exception): + """ There is a problem with a passed in request parameters. See returned message for details """ pass -class B2InvalidBucketConfiguration(Exception): - """ +class B2UnauthorizedError(Exception): + """ When calling b2_authorize_account, this means that there was something wrong with the accountId/applicationKeyId or with the applicationKey that was provided. The code unauthorized means that the application key is bad. The code unsupported means that the application key is only valid in a later version of the API. + + The code unauthorized means that the auth token is valid, but does not allow you to make this call with these parameters. When the code is either bad_auth_token or expired_auth_token you should call b2_authorize_account again to get a new auth token. """ pass -class B2AuthorizationError(Exception): - """ +class B2ForbiddenError(Exception): + """ You have a reached a storage cap limit, or account access may be impacted in some other way; see the human-readable message. """ pass -class B2BucketCreationError(Exception): - """ - """ +class B2RequestTimeoutError(Exception): + """ The service timed out trying to read your request. """ pass -class B2RequestError(Exception): - """ +class B2OutOfRangeError(Exception): + """ The Range header in the request is outside the size of the file.. """ + pass - """ -class B2InvalidRequestType(Exception): - """ +class B2TooManyRequestsError(Exception): + """ B2 may limit API requests on a per-account basis. """ + pass - """ -class B2FileNotFound(Exception): +class B2InternalError(Exception): + """ An unexpected error has occurred. """ + pass + + +class B2ServiceUnavailableError(Exception): + """ The service is temporarily unavailable. The human-readable message identifies the nature of the issue, in general we recommend retrying with an exponential backoff between retries in response to this error. """ + pass + - """ \ No newline at end of file +class B2InvalidBucketName(Exception): + """ Bucket name must be alphanumeric or '-' """ + pass + + +class B2InvalidBucketConfiguration(Exception): + """ Value error in bucket configuration """ + pass + +class B2AuthorizationError(Exception): + """ An error with the authorization request """ + pass + +class B2InvalidRequestType(Exception): + """ Request type must be get or post """ + pass diff --git a/b2blaze/b2lib.py b/b2blaze/b2lib.py index ecf5a54..a5eebdd 100644 --- a/b2blaze/b2lib.py +++ b/b2blaze/b2lib.py @@ -2,10 +2,8 @@ Copyright George Sibble 2018 """ import os -from b2blaze.b2_exceptions import B2ApplicationKeyNotSet, B2KeyIDNotSet, B2InvalidBucketName, B2InvalidBucketConfiguration -from b2blaze.b2_exceptions import B2BucketCreationError +from b2blaze.b2_exceptions import B2ApplicationKeyNotSet, B2KeyIDNotSet from b2blaze.connector import B2Connector - from b2blaze.models.bucket_list import B2Buckets class B2(object): diff --git a/b2blaze/connector.py b/b2blaze/connector.py index d8ff5d2..be4a3e0 100644 --- a/b2blaze/connector.py +++ b/b2blaze/connector.py @@ -4,7 +4,7 @@ import requests import datetime from requests.auth import HTTPBasicAuth -from b2blaze.b2_exceptions import B2AuthorizationError, B2RequestError, B2InvalidRequestType +from b2blaze.b2_exceptions import B2Exception, B2AuthorizationError, B2InvalidRequestType import sys from hashlib import sha1 from b2blaze.utilities import b2_url_encode, decode_error, get_content_length, StreamWithHashProgress @@ -68,7 +68,7 @@ def _authorize(self): 'Authorization': self.auth_token }) else: - raise B2AuthorizationError(decode_error(result)) + raise B2Exception.parse(result) def make_request(self, path, method='get', headers={}, params={}, account_id_required=False): diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index 8485700..fc629f0 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -3,7 +3,7 @@ """ from io import BytesIO from ..utilities import b2_url_encode, b2_url_decode, decode_error -from ..b2_exceptions import B2RequestError +from ..b2_exceptions import B2Exception from ..api import API class B2File(object): @@ -67,7 +67,7 @@ def get_versions(self, limit=None): new_file = B2File(connector=self.connector, parent_list=self, **file_json) file_versions.append(new_file) else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) return file_versions @@ -85,7 +85,7 @@ def hide(self): self.parent_list._files_by_name.pop(self.file_name) self.parent_list._files_by_id.pop(self.file_id) else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) def delete_all_versions(self, confirm=False): @@ -129,7 +129,7 @@ def delete_version(self): } response = self.connector.make_request(path=path, method='post', params=params) if not response.status_code == 200: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) self.deleted = True @@ -139,7 +139,7 @@ def download(self): if response.status_code == 200: return BytesIO(response.content) else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) @property def url(self): diff --git a/b2blaze/models/bucket.py b/b2blaze/models/bucket.py index befcc24..693ff32 100644 --- a/b2blaze/models/bucket.py +++ b/b2blaze/models/bucket.py @@ -2,8 +2,7 @@ Copyright George Sibble 2018 """ from .file_list import B2FileList -from ..b2_exceptions import B2RequestError -from ..utilities import decode_error +from ..b2_exceptions import B2Exception from ..api import API class B2Bucket(object): @@ -48,7 +47,7 @@ def delete(self, delete_files=False, confirm_non_empty=False): files = self.files.all(include_hidden=True) if delete_files: if not confirm_non_empty: - raise Exception('Bucket is not empty! Must confirm deletion of all files with confirm_non_empty=True') + raise B2Exception('Bucket is not empty! Must confirm deletion of all files with confirm_non_empty=True') else: print("Deleting all files from bucket. Beware API limits!") self.files.delete_all(confirm=True) @@ -63,7 +62,7 @@ def delete(self, delete_files=False, confirm_non_empty=False): del self.parent_list._buckets_by_name[self.bucket_name] del self.parent_list._buckets_by_id[self.bucket_id] else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) def edit(self): #TODO: Edit details diff --git a/b2blaze/models/bucket_list.py b/b2blaze/models/bucket_list.py index 618a62f..5ea558f 100644 --- a/b2blaze/models/bucket_list.py +++ b/b2blaze/models/bucket_list.py @@ -2,8 +2,7 @@ Copyright George Sibble 2018 """ -from ..b2_exceptions import B2InvalidBucketName, B2InvalidBucketConfiguration, B2BucketCreationError, B2RequestError -from ..utilities import decode_error +from ..b2_exceptions import B2Exception, B2InvalidBucketName, B2InvalidBucketConfiguration from .bucket import B2Bucket from ..api import API @@ -52,7 +51,7 @@ def _update_bucket_list(self, retrieve=False): if retrieve: return buckets else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) def get(self, bucket_name=None, bucket_id=None): """ @@ -76,7 +75,7 @@ def create(self, bucket_name, security, configuration=None): """ path = API.create_bucket if type(bucket_name) != str and type(bucket_name) != bytes: - raise B2InvalidBucketName + raise B2InvalidBucketName("Bucket name must be alphanumeric or '-") if type(configuration) != dict and configuration is not None: raise B2InvalidBucketConfiguration params = { @@ -94,4 +93,4 @@ def create(self, bucket_name, security, configuration=None): self._buckets_by_id[bucket_json['bucketId']] = new_bucket return new_bucket else: - raise B2RequestError(decode_error(response)) \ No newline at end of file + raise B2Exception.parse(response) \ No newline at end of file diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index 00e7422..b5f7297 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -2,10 +2,9 @@ Copyright George Sibble 2018 """ -from ..b2_exceptions import B2InvalidBucketName, B2InvalidBucketConfiguration, B2BucketCreationError from .b2_file import B2File from ..utilities import b2_url_encode, get_content_length, get_part_ranges, decode_error, RangeStream, StreamWithHashProgress -from ..b2_exceptions import B2RequestError, B2FileNotFound +from ..b2_exceptions import B2Exception from multiprocessing.dummy import Pool as ThreadPool from ..api import API @@ -58,7 +57,7 @@ def delete_all(self, confirm=True): for f in all_files: f.delete_all_versions(confirm=True) except Exception as E: - raise B2RequestError(decode_error(E)) + raise B2Exception.parse(E) return [] @@ -91,7 +90,7 @@ def _update_files_list(self, retrieve=False, limit=None): else: params['startFileName'] = files_json['nextFileName'] else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) if retrieve: return files @@ -189,7 +188,7 @@ def all_file_versions(self, limit=None): else: params['startFileName'] = files_json['nextFileName'] else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) return {'file_names': file_names, 'file_versions': file_versions, 'file_ids': file_ids} @@ -202,14 +201,11 @@ def _get_by_name(self, file_name): } response = self.connector.make_request(path, method='post', params=params) - if response.status_code == 200: - file_json = response.json() - if len(file_json['files']) > 0: - return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) - else: - raise B2FileNotFound('fileName - ' + file_name) + file_json = response.json() + if response.status_code == 200 and len(file_json['files']) > 0: + return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) def _get_by_id(self, file_id): """ Internal method, return file by file id """ @@ -222,7 +218,7 @@ def _get_by_id(self, file_id): file_json = response.json() return B2File(connector=self.connector, parent_list=self, **file_json) else: - raise B2RequestError(decode_error(response)) + raise B2Exception.parse(response) def upload(self, contents, file_name, mime_content_type=None, content_length=None, progress_listener=None): @@ -254,9 +250,9 @@ def upload(self, contents, file_name, mime_content_type=None, content_length=Non self._update_files_list() return new_file else: - raise B2RequestError(decode_error(upload_response)) + raise B2Exception.parse(upload_response) else: - raise B2RequestError(decode_error(upload_url_response)) + raise B2Exception.parse(upload_url_response) def upload_large_file(self, contents, file_name, part_size=None, num_threads=4, mime_content_type=None, content_length=None, progress_listener=None): @@ -307,9 +303,9 @@ def upload_part_worker(args): if upload_part_response.status_code == 200: return upload_part_response.json().get('contentSha1', None) else: - raise B2RequestError(decode_error(upload_part_response)) + raise B2Exception.parse(upload_part_response) else: - raise B2RequestError(decode_error(upload_part_url_response)) + raise B2Exception.parse(upload_part_url_response) sha_list = pool.map(upload_part_worker, enumerate(get_part_ranges(content_length, part_size), 1)) pool.close() pool.join() @@ -323,6 +319,6 @@ def upload_part_worker(args): new_file = B2File(connector=self.connector, parent_list=self, **finish_large_file_response.json()) return new_file else: - raise B2RequestError(decode_error(finish_large_file_response)) + raise B2Exception.parse(finish_large_file_response) else: - raise B2RequestError(decode_error(large_file_response)) + raise B2Exception.parse(large_file_response) diff --git a/tests.py b/tests.py index 061ba5c..2350c0d 100644 --- a/tests.py +++ b/tests.py @@ -6,7 +6,7 @@ from sure import expect from datetime import datetime import pytest -from b2blaze.b2_exceptions import B2RequestError, B2FileNotFound +from b2blaze.b2_exceptions import B2Exception, B2RequestError class TestB2(object): """ Tests for the b2blaze library """ @@ -184,10 +184,11 @@ def test_bucket_get_file_versions_by_id(self): @pytest.mark.files + @pytest.mark.b2errors def test_get_file_doesnt_exist(self): - """ Get file which doens't exist should raise B2FileNotFound """ + """ Get file which doens't exist should raise B2RequestError """ bucket = self.getbucket() - with pytest.raises(B2FileNotFound): + with pytest.raises(B2RequestError): file = bucket.files.get(file_name='nope.txt') with pytest.raises(B2RequestError): file2 = bucket.files.get(file_id='abcd') @@ -216,6 +217,7 @@ def test_download_url(self): @pytest.mark.files + @pytest.mark.b2errors def test_hide_file(self): """ Should create + upload, then hide / soft-delete a file by name. File should no longer be returned when searched by name in bucket. @@ -229,7 +231,7 @@ def test_hide_file(self): upload.hide() # Refresh bucket; getting the the file should fail - with pytest.raises(B2FileNotFound): + with pytest.raises(B2RequestError): bucket = self.getbucket() file = bucket.files.get(file_name=upload.file_name) assert not file, 'Deleted file should not be in files list' @@ -283,7 +285,7 @@ def test_delete_all_file_versions(self): file.delete_all_versions(confirm=True) # Refresh bucket; getting the the file should fail - with pytest.raises(B2FileNotFound): + with pytest.raises(B2RequestError): bucket = self.getbucket() file2 = bucket.files.get(file_name=file.file_name) assert not file2, 'Deleted all file versions, file should not exist' From 9cfe03448060fa7d1f551c028420f31a8a94ce4d Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Tue, 30 Oct 2018 15:54:27 +0800 Subject: [PATCH 12/19] tests.py - All tests green --- b2blaze/b2_exceptions.py | 28 +++++++++++++++++----------- b2blaze/models/file_list.py | 16 ++++++++++------ tests.py | 10 +++++----- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/b2blaze/b2_exceptions.py b/b2blaze/b2_exceptions.py index d229757..ff28887 100644 --- a/b2blaze/b2_exceptions.py +++ b/b2blaze/b2_exceptions.py @@ -4,6 +4,7 @@ import json + class B2ApplicationKeyNotSet(Exception): """ You must set the B2_KEY_ID environment variable before running the application """ pass @@ -20,12 +21,8 @@ class B2Exception(Exception): @staticmethod def parse(response): """ Parse the response error code and return the related error type. """ - response_json = response.json() - message = response_json['message'] - code = response_json['code'] - status = int(response_json['status']) - exception_map = { + API_EXCEPTION_CODES = { 400 : B2RequestError, 401 : B2UnauthorizedError, 403 : B2ForbiddenError, @@ -35,13 +32,22 @@ def parse(response): 500 : B2InternalError, 503 : B2ServiceUnavailableError, } - - # Return B2Exception if unrecognized status code - if not status in exception_map: - return B2Exception('{} - {}: {}'.format(status, code, message)) - ErrorClass = exception_map[status] - return ErrorClass('{} - {}: {}'.format(status, code, message)) + try: + response_json = response.json() + message = response_json['message'] + code = response_json['code'] + status = int(response_json['status']) + + # Return B2Exception if unrecognized status code + if not status in API_EXCEPTION_CODES: + return B2Exception('{} - {}: {}'.format(status, code, message)) + + ErrorClass = API_EXCEPTION_CODES[status] + return ErrorClass('{} - {}: {}'.format(status, code, message)) + + except: + return Exception('error parsing response. status code - {} Response JSON: {}'.format(response.status_code, response_json) ) class B2FileNotFoundError(Exception): """ 404 Not Found """ diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index b5f7297..0a2040a 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -4,7 +4,7 @@ from .b2_file import B2File from ..utilities import b2_url_encode, get_content_length, get_part_ranges, decode_error, RangeStream, StreamWithHashProgress -from ..b2_exceptions import B2Exception +from ..b2_exceptions import B2Exception, B2FileNotFoundError from multiprocessing.dummy import Pool as ThreadPool from ..api import API @@ -193,7 +193,7 @@ def all_file_versions(self, limit=None): def _get_by_name(self, file_name): - """ Internal method, return file by file name """ + """ Internal method, return single file by file name """ path = API.list_all_files params = { 'prefix': b2_url_encode(file_name), @@ -202,13 +202,17 @@ def _get_by_name(self, file_name): response = self.connector.make_request(path, method='post', params=params) file_json = response.json() - if response.status_code == 200 and len(file_json['files']) > 0: - return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) - else: + + # Handle errors and empty files + if not response.status_code == 200: raise B2Exception.parse(response) + if not len(file_json['files']) > 0: + raise B2FileNotFoundError('Filename {} not found'.format(file_name)) + else: + return B2File(connector=self.connector, parent_list=self, **file_json['files'][0]) def _get_by_id(self, file_id): - """ Internal method, return file by file id """ + """ Internal method, return single file by file id """ path = API.file_info params = { 'fileId': file_id diff --git a/tests.py b/tests.py index 2350c0d..a9fcace 100644 --- a/tests.py +++ b/tests.py @@ -6,7 +6,7 @@ from sure import expect from datetime import datetime import pytest -from b2blaze.b2_exceptions import B2Exception, B2RequestError +from b2blaze.b2_exceptions import B2Exception, B2RequestError, B2FileNotFoundError class TestB2(object): """ Tests for the b2blaze library """ @@ -186,9 +186,9 @@ def test_bucket_get_file_versions_by_id(self): @pytest.mark.files @pytest.mark.b2errors def test_get_file_doesnt_exist(self): - """ Get file which doens't exist should raise B2RequestError """ + """ Get file which doesn't exist should raise B2FileNotFoundError, get by ID should raise B2RequestError """ bucket = self.getbucket() - with pytest.raises(B2RequestError): + with pytest.raises(B2FileNotFoundError): file = bucket.files.get(file_name='nope.txt') with pytest.raises(B2RequestError): file2 = bucket.files.get(file_id='abcd') @@ -231,7 +231,7 @@ def test_hide_file(self): upload.hide() # Refresh bucket; getting the the file should fail - with pytest.raises(B2RequestError): + with pytest.raises(B2FileNotFoundError): bucket = self.getbucket() file = bucket.files.get(file_name=upload.file_name) assert not file, 'Deleted file should not be in files list' @@ -285,7 +285,7 @@ def test_delete_all_file_versions(self): file.delete_all_versions(confirm=True) # Refresh bucket; getting the the file should fail - with pytest.raises(B2RequestError): + with pytest.raises(B2FileNotFoundError): bucket = self.getbucket() file2 = bucket.files.get(file_name=file.file_name) assert not file2, 'Deleted all file versions, file should not exist' From 679e880e18aff2d6335f3cf794bf63d5c42e2266 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Tue, 30 Oct 2018 16:16:33 +0800 Subject: [PATCH 13/19] Version increment to 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12ea8b2..79b6505 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.1.11' +VERSION = '0.2.0' from os import path this_directory = path.abspath(path.dirname(__file__)) From 5dd7f5626640857c2c3e5d1b42cd1fb5142b8dad Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Tue, 30 Oct 2018 22:34:07 +0800 Subject: [PATCH 14/19] Replace sure.expect with assert ; removed from requirements.txt --- requirements.txt | 4 ---- tests.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 81f557a..3fd3082 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,10 @@ certifi==2018.10.15 chardet==3.0.4 coverage==4.5.1 idna==2.7 -mock==2.0.0 more-itertools==4.3.0 nose==1.3.7 -pbr==5.1.0 pluggy==0.6.0 py==1.5.4 pytest==3.6.2 requests==2.19.1 -six==1.11.0 -sure==1.4.11 urllib3==1.23 diff --git a/tests.py b/tests.py index a9fcace..9ccbaec 100644 --- a/tests.py +++ b/tests.py @@ -2,8 +2,6 @@ Copyright George Sibble 2018 """ import b2blaze.b2lib -import sure -from sure import expect from datetime import datetime import pytest from b2blaze.b2_exceptions import B2Exception, B2RequestError, B2FileNotFoundError @@ -70,7 +68,7 @@ def test_get_bucket(self): def test_get_all_buckets(self): """ Get buckets. Number of buckets returned should be >1 """ buckets = self.b2.buckets.all() - expect(len(buckets)).should.be.greater_than(1) + assert len(buckets) > 1, "Number of buckets returned should be >1" @pytest.mark.bucket def test_get_nonexistent_bucket(self): From 8c91ca70a2438550c61f4a05777c19bb86ec2638 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 31 Oct 2018 02:17:37 +0800 Subject: [PATCH 15/19] file_list: fix confirm delete_all default False --- b2blaze/models/file_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2blaze/models/file_list.py b/b2blaze/models/file_list.py index 0a2040a..ecf8c1e 100644 --- a/b2blaze/models/file_list.py +++ b/b2blaze/models/file_list.py @@ -44,7 +44,7 @@ def all(self, include_hidden=False, limit=None): return files return [] # Return empty set on no results - def delete_all(self, confirm=True): + def delete_all(self, confirm=False): """ Delete all files in the bucket. Parameters: confirm: (bool) Safety check. Confirm deletion From 4c51ce053c4bd7d53ae48de70751acd9bba9cdcd Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 31 Oct 2018 02:39:18 +0800 Subject: [PATCH 16/19] Increment version number, add changelog --- changelog.md | 33 +++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e692ef8 --- /dev/null +++ b/changelog.md @@ -0,0 +1,33 @@ + +## Changelog + +#### v0.2.1 + +**Python version update** +Python version: +- Python 3 required! + +**Breaking API changes:** + - (B2File) delete: changed to delete_version + +**Nonbreaking API changes:** + +B2File: +- hide added +- delete_version added +- delete_all_versions(confirm=False) added + +B2FileList +- all(introduced new parameter: include_hidden) +- delete_all(confirm=False) added +- get_versions(file_name=None, file_id=None, limit=None) added +- all_file_versions(limit=None) added + +b2_exceptions.py +- Changed API error classes to match Backblaze API docs +- added base API exception B2Exception. +- Handle non-200 status code responses with 'raise B2Exception.parse(response)` <-- would be happy to think up a better way to handle this + +tests.py +- added and cleaned up integration tests +- tests can now be run with `python tests.py` diff --git a/setup.py b/setup.py index 79b6505..5d33deb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.2.0' +VERSION = '0.2.1' from os import path this_directory = path.abspath(path.dirname(__file__)) From 006a528b5f1d9da5e42992e1a4c5b2c54ef18cb5 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 31 Oct 2018 02:40:37 +0800 Subject: [PATCH 17/19] add changelog --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index e692ef8..ebf42e1 100644 --- a/changelog.md +++ b/changelog.md @@ -26,7 +26,7 @@ B2FileList b2_exceptions.py - Changed API error classes to match Backblaze API docs - added base API exception B2Exception. -- Handle non-200 status code responses with 'raise B2Exception.parse(response)` <-- would be happy to think up a better way to handle this +- Handle non-200 status code responses with 'raise B2Exception.parse(response)` tests.py - added and cleaned up integration tests From a12c6fadbc61640daf398a1a11649e2950a33ba5 Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 31 Oct 2018 04:14:06 +0800 Subject: [PATCH 18/19] b2_file: rename B2File.delete_version -> delete --- README.md | 2 +- b2blaze/models/b2_file.py | 4 ++-- changelog.md | 4 ---- tests.py | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4abaf90..ed0089f 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ save_file.close() #### Delete a file version ```python -file.delete_version() +file.delete() ``` This deletes a single version of a file. (See the [docs on File Versions](https://www.backblaze.com/b2/docs/b2_delete_file_version.html) at Backblaze for explanation) diff --git a/b2blaze/models/b2_file.py b/b2blaze/models/b2_file.py index fc629f0..320ca34 100644 --- a/b2blaze/models/b2_file.py +++ b/b2blaze/models/b2_file.py @@ -117,10 +117,10 @@ def delete_all_versions(self, confirm=False): print(version_count, 'file versions') for count, v in enumerate(versions): print('deleting [{}/{}]'.format(count + 1 , version_count)) - v.delete_version() + v.delete() - def delete_version(self): + def delete(self): """ Delete a file version (Does not delete entire file history: only most recent version) """ path = API.delete_file_version params = { diff --git a/changelog.md b/changelog.md index ebf42e1..e82f12d 100644 --- a/changelog.md +++ b/changelog.md @@ -7,14 +7,10 @@ Python version: - Python 3 required! -**Breaking API changes:** - - (B2File) delete: changed to delete_version - **Nonbreaking API changes:** B2File: - hide added -- delete_version added - delete_all_versions(confirm=False) added B2FileList diff --git a/tests.py b/tests.py index 9ccbaec..9f051cd 100644 --- a/tests.py +++ b/tests.py @@ -253,7 +253,7 @@ def test_delete_file_version(self): # Delete version print('test_delete_file_version: file_name', file.file_name) print('test_delete_file_version: file_id', file.file_id) - file.delete_version() + file.delete() # Refresh bucket; getting the the file should fail file2 = bucket.files.get(file_name=file.file_name) From cbb91876409ded095ff444061db8bafdb08987fc Mon Sep 17 00:00:00 2001 From: Matt Fields Date: Wed, 31 Oct 2018 04:24:18 +0800 Subject: [PATCH 19/19] tests.py: fix delete_all confirmation --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 9f051cd..ce026a9 100644 --- a/tests.py +++ b/tests.py @@ -324,7 +324,7 @@ def test_bucket_delete_all_files(self): assert len(files) > 0, 'Bucket should still contain files' # Delete all files - bucket.files.delete_all() + bucket.files.delete_all(confirm=True) assert len(bucket.files.all()) == 0, 'Bucket should be empty'